// 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 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(&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& 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(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& VertexPositions, TArray& OutTriangles); void ExtrudeZSimplePolygon(const TArray& InVertices, float BottomZ, float TopZ, TArray& OutVertices, TArray& 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& VertexPositions, TArray& 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 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 SplineVerts; AWaterBody* OwningBody = GetTypedOuter(); 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 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 CorrectedSplineVertices; FGeomTools2D::CorrectPolygonWinding(CorrectedSplineVertices, SplineVerts, false); TArray TriangulatedPolygonVertices; FGeomTools2D::TriangulatePoly(/*out*/TriangulatedPolygonVertices, SplineVerts, false); TArray OutCleanTris; FGeomTools2D::RemoveRedundantTriangles(OutCleanTris, TriangulatedPolygonVertices); TArray> ConvexHulls; FGeomTools2D::GenerateConvexPolygonsFromTriangles(ConvexHulls, OutCleanTris); { TRACE_CPUPROFILER_EVENT_SCOPE(GenerateHull); for (const auto& Hull : ConvexHulls) { TArray Hull3DVerts; Hull3DVerts.Reserve(Hull.Num()); for (int32 PointIdx = 0; PointIdx < Hull.Num(); ++PointIdx) { Hull3DVerts.Emplace(FVector(Hull[PointIdx], 0)); } if(Hull3DVerts.Num() > 2) { TArray ExtrudedVerts; TArray 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(); if (CachedBodySetup && OwningBody && OwningBody->GetWaterBodyComponent()) { FTransform GeomTransform(GetComponentTransform()); GeomTransform.AddToTranslation(OwningBody->GetWaterBodyComponent()->GetWaterNavCollisionOffset()); GeomExport.ExportRigidBodySetup(*CachedBodySetup, GeomTransform); return false; } return true; }