Files
UnrealEngine/Engine/Plugins/Experimental/Water/Source/Runtime/Private/LakeCollisionComponent.cpp
2025-05-18 13:04:45 +08:00

465 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LakeCollisionComponent.h"
#include "BodySetupEnums.h"
#include "SceneView.h"
#include "WaterBodyActor.h"
#include "Physics/PhysicsInterfaceTypes.h"
#include "WaterSplineComponent.h"
#include "GeomTools.h"
#include "PrimitiveSceneProxy.h"
#include "PhysicsEngine/BodySetup.h"
#include "AI/NavigationSystemHelpers.h"
#include "PrimitiveViewRelevance.h"
#include "SceneManagement.h"
#include "ShaderCore.h"
#include "WaterUtils.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(LakeCollisionComponent)
// ----------------------------------------------------------------------------------
extern TAutoConsoleVariable<float> CVarWaterSplineResampleMaxDistance;
// ----------------------------------------------------------------------------------
ULakeCollisionComponent::ULakeCollisionComponent(const FObjectInitializer& ObjectInitializer)
: UPrimitiveComponent(ObjectInitializer)
{
bHiddenInGame = true;
bCastDynamicShadow = false;
bIgnoreStreamingManagerUpdate = true;
bUseEditorCompositing = true;
}
void ULakeCollisionComponent::UpdateCollision(FVector InBoxExtent, bool bSplinePointsChanged)
{
bool bNeedsUpdatedBody = bSplinePointsChanged || CachedBodySetup == nullptr;
if (BoxExtent != InBoxExtent)
{
bNeedsUpdatedBody = true;
BoxExtent = InBoxExtent;
UpdateBounds();
}
if (bNeedsUpdatedBody)
{
UpdateBodySetup();
}
if (bPhysicsStateCreated)
{
// Update physics engine collision shapes
BodyInstance.UpdateBodyScale(GetComponentTransform().GetScale3D(), true);
}
}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
FPrimitiveSceneProxy* ULakeCollisionComponent::CreateSceneProxy()
{
/** Represents a ULakeCollisionComponent to the scene manager. */
class FLakeCollisionSceneProxy final : public FPrimitiveSceneProxy
{
public:
SIZE_T GetTypeHash() const override
{
static size_t UniquePointer;
return reinterpret_cast<size_t>(&UniquePointer);
}
FLakeCollisionSceneProxy(const ULakeCollisionComponent* InComponent)
: FPrimitiveSceneProxy(InComponent)
{
bWillEverBeLit = false;
if (InComponent->CachedBodySetup)
{
// copy the geometry for being able to access it on the render thread :
AggregateGeom = InComponent->CachedBodySetup->AggGeom;
}
}
virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
{
const FMatrix& LocalToWorld = GetLocalToWorld();
const FTransform LocalToWorldTransform(LocalToWorld);
const bool bDrawCollision = ViewFamily.EngineShowFlags.Collision && IsCollisionEnabled();
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
if (VisibilityMap & (1 << ViewIndex))
{
const FSceneView* View = Views[ViewIndex];
if (bDrawCollision && AllowDebugViewmodes())
{
FColor CollisionColor(157, 149, 223, 255);
const bool bPerHullColor = false;
const bool bDrawSolid = false;
AggregateGeom.GetAggGeom(LocalToWorldTransform, GetSelectionColor(CollisionColor, IsSelected(), IsHovered()).ToFColor(true), nullptr, bPerHullColor, bDrawSolid, AlwaysHasVelocity(), ViewIndex, Collector);
}
RenderBounds(Collector.GetPDI(ViewIndex), View->Family->EngineShowFlags, GetBounds(), IsSelected());
}
}
}
virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override
{
// Should we draw this because collision drawing is enabled, and we have collision
const bool bShowForCollision = View->Family->EngineShowFlags.Collision && IsCollisionEnabled();
FPrimitiveViewRelevance Result;
Result.bDrawRelevance = IsShown(View) || bShowForCollision;
Result.bDynamicRelevance = true;
Result.bShadowRelevance = false;
Result.bEditorPrimitiveRelevance = UseEditorCompositing(View);
return Result;
}
virtual uint32 GetMemoryFootprint(void) const override { return(sizeof(*this) + GetAllocatedSize()); }
uint32 GetAllocatedSize(void) const { return FPrimitiveSceneProxy::GetAllocatedSize() + AggregateGeom.GetAllocatedSize(); }
private:
FKAggregateGeom AggregateGeom;
};
return new FLakeCollisionSceneProxy(this);
}
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
FBoxSphereBounds ULakeCollisionComponent::CalcBounds(const FTransform& LocalToWorld) const
{
return FBoxSphereBounds(FBox(-BoxExtent, BoxExtent)).TransformBy(LocalToWorld);
}
void ULakeCollisionComponent::CreateLakeBodySetupIfNeeded()
{
if (!IsValid(CachedBodySetup))
{
CachedBodySetup = NewObject<UBodySetup>(this, TEXT("BodySetup")); // a name needs to be provided to ensure determinism
CachedBodySetup->CollisionTraceFlag = CTF_UseSimpleAsComplex;
// HACK [jonathan.bard] : to avoid non-determinitic cook issues which can occur as new collision components are created on construction, which generates a random GUID for UBodySetup,
// we use a GUID based on the (deterministic) full name of the component, tweaked so as not to collide with standard GUIDs. BodySetupGuid should be removed altogether and UBodySetup's DDC
// key should be based only on its actual content but for now, this is one way around the determinism issue :
CachedBodySetup->BodySetupGuid = FWaterUtils::StringToGuid(GetFullName(nullptr, EObjectFullNameFlags::IncludeClassPackage));
}
}
bool TriangulateSimpleXYPlanarPolygon(const TArray<FVector>& VertexPositions, TArray<FIntVector>& OutTriangles);
void ExtrudeZSimplePolygon(const TArray<FVector>& InVertices, float BottomZ, float TopZ,
TArray<FVector>& OutVertices, TArray<FIntVector>& OutTriangles)
{
int32 InVerticesNum = InVertices.Num();
if (InVerticesNum < 3) // degenerate case w/ zero volume; make empty mesh
{
OutVertices.Reset();
OutTriangles.Reset();
return;
}
// triangulate top/bottom shape
bool bTrianglesAreClockwise = TriangulateSimpleXYPlanarPolygon(InVertices, OutTriangles);
if (bTrianglesAreClockwise)
{
Swap(BottomZ, TopZ);
}
int32 TopTriNum = OutTriangles.Num();
// set vertices
OutVertices.SetNum(2 * InVerticesNum);
for (int32 InIdx = 0; InIdx < InVerticesNum; ++InIdx)
{
OutVertices[InIdx] = InVertices[InIdx];
OutVertices[InIdx].Z = BottomZ;
OutVertices[InVerticesNum + InIdx] = InVertices[InIdx];
OutVertices[InVerticesNum + InIdx].Z = TopZ;
}
// set triangles
OutTriangles.SetNum(TopTriNum * 2 + InVerticesNum * 2); // top and bottom are placed first, then sides
for (int32 TriIdx = 0; TriIdx < TopTriNum; TriIdx++)
{
FIntVector& TopTri = OutTriangles[TopTriNum + TriIdx];
const FIntVector& BottomTri = OutTriangles[TriIdx];
TopTri.X = BottomTri.X + InVerticesNum;
// Y,Z intentionally swizzled to reverse triangle orientation for top vs bottom
TopTri.Y = BottomTri.Z + InVerticesNum;
TopTri.Z = BottomTri.Y + InVerticesNum;
}
// offsets for the clockwise and counter-clockwise vertices
for (int32 NextIdx = 0, LastIdx = InVerticesNum - 1, SideTriIdx = TopTriNum * 2; NextIdx < InVerticesNum; LastIdx = NextIdx++)
{
OutTriangles[SideTriIdx++] = FIntVector(InVerticesNum + LastIdx, InVerticesNum + NextIdx, NextIdx);
OutTriangles[SideTriIdx++] = FIntVector(InVerticesNum + LastIdx, NextIdx, LastIdx);
}
}
/**
* Triangulate a polygon as projected to the XY plane, using ear clipping. Orientation of triangles will match orientation of input curve
* Adapted from TriangulateSimplePolygon in GeometryProcessing's PolygonTriangulation.cpp, to avoid using any types/functions in GeometryProcessing
*
* @return bool indicating orientation of output triangles
*/
bool TriangulateSimpleXYPlanarPolygon(const TArray<FVector>& VertexPositions, TArray<FIntVector>& OutTriangles)
{
// helper functions for analyzing XY-projected triangles
struct Local
{
// returns 2*signed_area of the triangle formed by pts A, B, C
static inline float XYArea2(const FVector& A, const FVector& B, const FVector& C)
{
return (A.X*B.Y - A.Y*B.X) + (B.X*C.Y - B.Y*C.X) + (C.X*A.Y - C.Y*A.X);
}
static inline bool XYIsTriangleFlipped(float OrientationSign, const FVector& A, const FVector& B, const FVector& C)
{
float XYSignedDoubleArea = XYArea2(A, B, C);
return XYSignedDoubleArea * OrientationSign < 0;
}
static inline bool XYIsInsideTriangle(const FVector& A, const FVector& B, const FVector& C, const FVector& P)
{
float Sign1 = XYArea2(A, B, P);
float Sign2 = XYArea2(B, C, P);
float Sign3 = XYArea2(C, A, P);
return (Sign1*Sign2 > 0) && (Sign2*Sign3 > 0) && (Sign3*Sign1 > 0); // true if all same (and non-zero) sign
}
};
// Polygon must have at least three vertices/edges
int32 PolygonVertexCount = VertexPositions.Num();
check(PolygonVertexCount >= 3);
// compute signed area of polygon
double PolySignedArea = 0;
for (int32 Idx = 0, LastIdx = PolygonVertexCount - 1; Idx < PolygonVertexCount; LastIdx = Idx++)
{
const FVector& v1 = VertexPositions[LastIdx];
const FVector& v2 = VertexPositions[Idx];
PolySignedArea += v1.X*v2.Y - v1.Y*v2.X;
}
PolySignedArea *= 0.5;
bool bIsClockwise = PolySignedArea < 0;
double OrientationSign = (bIsClockwise) ? -1.0 : 1.0;
OutTriangles.Reset();
// If perimeter has 3 vertices, just copy content of perimeter out
if (PolygonVertexCount == 3)
{
OutTriangles.Add(FIntVector(0, 1, 2));
return bIsClockwise;
}
// Make a simple linked list array of the previous and next vertex numbers, for each vertex number
// in the polygon. This will just save us having to iterate later on.
static TArray<int32> PrevVertexNumbers, NextVertexNumbers;
PrevVertexNumbers.SetNumUninitialized(PolygonVertexCount, EAllowShrinking::No);
NextVertexNumbers.SetNumUninitialized(PolygonVertexCount, EAllowShrinking::No);
for (int32 VertexNumber = 0; VertexNumber < PolygonVertexCount; ++VertexNumber)
{
PrevVertexNumbers[VertexNumber] = VertexNumber - 1;
NextVertexNumbers[VertexNumber] = VertexNumber + 1;
}
PrevVertexNumbers[0] = PolygonVertexCount - 1;
NextVertexNumbers[PolygonVertexCount - 1] = 0;
int32 EarVertexNumber = 0;
int32 EarTestCount = 0;
for (int32 RemainingVertexCount = PolygonVertexCount; RemainingVertexCount >= 3; )
{
bool bIsEar = true;
// If we're down to only a triangle, just treat it as an ear. Also, if we've tried every possible candidate
// vertex looking for an ear, go ahead and just treat the current vertex as an ear. This can happen when
// vertices are collinear or other degenerate cases.
if (RemainingVertexCount > 3 && EarTestCount < RemainingVertexCount)
{
const FVector& PrevVertexPosition = VertexPositions[PrevVertexNumbers[EarVertexNumber]];
const FVector& EarVertexPosition = VertexPositions[EarVertexNumber];
const FVector& NextVertexPosition = VertexPositions[NextVertexNumbers[EarVertexNumber]];
// Figure out whether the potential ear triangle is facing the same direction as the polygon
// itself. If it's facing the opposite direction, then we're dealing with a concave triangle
// and we'll skip it for now.
if (!Local::XYIsTriangleFlipped(
OrientationSign, PrevVertexPosition, EarVertexPosition, NextVertexPosition))
{
int32 TestVertexNumber = NextVertexNumbers[NextVertexNumbers[EarVertexNumber]];
do
{
// Test every other remaining vertex to make sure that it doesn't lie inside our potential ear
// triangle. If we find a vertex that's inside the triangle, then it cannot actually be an ear.
const FVector& TestVertexPosition = VertexPositions[TestVertexNumber];
if (Local::XYIsInsideTriangle(PrevVertexPosition, EarVertexPosition, NextVertexPosition, TestVertexPosition))
{
bIsEar = false;
break;
}
TestVertexNumber = NextVertexNumbers[TestVertexNumber];
} while (TestVertexNumber != PrevVertexNumbers[EarVertexNumber]);
}
else
{
bIsEar = false;
}
}
if (bIsEar)
{
// OK, we found an ear! Let's save this triangle in our output buffer.
{
FIntVector& Triangle = OutTriangles.Emplace_GetRef();
Triangle.X = PrevVertexNumbers[EarVertexNumber];
Triangle.Y = EarVertexNumber;
Triangle.Z = NextVertexNumbers[EarVertexNumber];
}
// Update our linked list. We're effectively cutting off the ear by pointing the ear vertex's neighbors to
// point at their next sequential neighbor, and reducing the remaining vertex count by one.
{
NextVertexNumbers[PrevVertexNumbers[EarVertexNumber]] = NextVertexNumbers[EarVertexNumber];
PrevVertexNumbers[NextVertexNumbers[EarVertexNumber]] = PrevVertexNumbers[EarVertexNumber];
--RemainingVertexCount;
}
// Move on to the previous vertex in the list, now that this vertex was cut
EarVertexNumber = PrevVertexNumbers[EarVertexNumber];
EarTestCount = 0;
}
else
{
// The vertex is not the ear vertex, because it formed a triangle that either had a normal which pointed in the opposite direction
// of the polygon, or at least one of the other polygon vertices was found to be inside the triangle. Move on to the next vertex.
EarVertexNumber = NextVertexNumbers[EarVertexNumber];
// Keep track of how many ear vertices we've tested, so that if we exhaust all remaining vertices, we can
// fall back to clipping the triangle and adding it to our mesh anyway. This is important for degenerate cases.
++EarTestCount;
}
}
ensure(OutTriangles.Num() > 0);
return bIsClockwise;
}
void ULakeCollisionComponent::UpdateBodySetup()
{
TRACE_CPUPROFILER_EVENT_SCOPE(ULakeCollisionComponent::UpdateBodySetup);
CreateLakeBodySetupIfNeeded();
FGuid PreviousBodySetupGuid = CachedBodySetup->BodySetupGuid;
CachedBodySetup->RemoveSimpleCollision();
// Removing the collision will needlessly generate a new Guid : restore the old one if valid to avoid invalidating the DDC :
if (PreviousBodySetupGuid.IsValid())
{
CachedBodySetup->BodySetupGuid = PreviousBodySetupGuid;
}
FMemMark Mark(FMemStack::Get());
TArray<FVector2D> SplineVerts;
AWaterBody* OwningBody = GetTypedOuter<AWaterBody>();
if (OwningBody && OwningBody->GetWaterSpline())
{
const UWaterSplineComponent* SplineComp = OwningBody->GetWaterSpline();
const float MaxZ = -BoxExtent.Z;
const float MinZ = BoxExtent.Z;
// Generate planes
const int32 NumPoints = SplineComp->GetNumberOfSplinePoints();
// lakes are closed loops so add 1 to the end
const int32 NumSteps = NumPoints + 1;
{
TRACE_CPUPROFILER_EVENT_SCOPE(ResampleSpline);
TArray<FVector> PolyLineVertices;
SplineComp->ConvertSplineToPolyLine(ESplineCoordinateSpace::World, FMath::Square(CVarWaterSplineResampleMaxDistance.GetValueOnGameThread()), PolyLineVertices);
// Transform to local space of this component :
Algo::Transform(PolyLineVertices, SplineVerts, [this](const FVector& Vertex) { return FVector2D(GetComponentToWorld().InverseTransformPosition(Vertex)); });
}
TArray<FVector2D> CorrectedSplineVertices;
FGeomTools2D::CorrectPolygonWinding(CorrectedSplineVertices, SplineVerts, false);
TArray<FVector2D> TriangulatedPolygonVertices;
FGeomTools2D::TriangulatePoly(/*out*/TriangulatedPolygonVertices, SplineVerts, false);
TArray<FVector2D> OutCleanTris;
FGeomTools2D::RemoveRedundantTriangles(OutCleanTris, TriangulatedPolygonVertices);
TArray<TArray<FVector2D>> ConvexHulls;
FGeomTools2D::GenerateConvexPolygonsFromTriangles(ConvexHulls, OutCleanTris);
{
TRACE_CPUPROFILER_EVENT_SCOPE(GenerateHull);
for (const auto& Hull : ConvexHulls)
{
TArray<FVector> Hull3DVerts;
Hull3DVerts.Reserve(Hull.Num());
for (int32 PointIdx = 0; PointIdx < Hull.Num(); ++PointIdx)
{
Hull3DVerts.Emplace(FVector(Hull[PointIdx], 0));
}
if(Hull3DVerts.Num() > 2)
{
TArray<FVector> ExtrudedVerts;
TArray<FIntVector> Indices;
ExtrudeZSimplePolygon(Hull3DVerts, MinZ, MaxZ, ExtrudedVerts, Indices);
FKConvexElem Convex;
Convex.VertexData = MoveTemp(ExtrudedVerts);
Convex.UpdateElemBox();
CachedBodySetup->AggGeom.ConvexElems.Add(Convex);
}
}
}
CachedBodySetup->CreatePhysicsMeshes();
RecreatePhysicsState();
MarkRenderStateDirty();
}
}
UBodySetup* ULakeCollisionComponent::GetBodySetup()
{
return CachedBodySetup;
}
// Apply offset (substract MaxWaveHeight) to the Lake collision so nav mesh geometry is exported at ground level
bool ULakeCollisionComponent::DoCustomNavigableGeometryExport(FNavigableGeometryExport& GeomExport) const
{
const AWaterBody* OwningBody = GetTypedOuter<AWaterBody>();
if (CachedBodySetup && OwningBody && OwningBody->GetWaterBodyComponent())
{
FTransform GeomTransform(GetComponentTransform());
GeomTransform.AddToTranslation(OwningBody->GetWaterBodyComponent()->GetWaterNavCollisionOffset());
GeomExport.ExportRigidBodySetup(*CachedBodySetup, GeomTransform);
return false;
}
return true;
}