// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= GeomFitUtils.cpp: Utilities for fitting collision models to static meshes. =============================================================================*/ #include "GeomFitUtils.h" #include "EngineDefines.h" #include "Misc/MessageDialog.h" #include "UObject/UObjectIterator.h" #include "Components/StaticMeshComponent.h" #include "Model.h" #include "Engine/Polys.h" #include "StaticMeshResources.h" #include "EditorSupportDelegates.h" #include "BSPOps.h" #include "RawMesh.h" #include "PhysicsEngine/BoxElem.h" #include "PhysicsEngine/SphereElem.h" #include "PhysicsEngine/SphylElem.h" #include "PhysicsEngine/BodySetup.h" #include "Engine/StaticMesh.h" #include "MeshDescription.h" #include "StaticMeshAttributes.h" #include "Settings/EditorExperimentalSettings.h" #define LOCAL_EPS (0.01f) static void AddVertexIfNotPresent(TArray& vertices, FVector& newVertex) { bool isPresent = 0; for (int32 i = 0; i < vertices.Num() && !isPresent; i++) { float diffSqr = (newVertex - vertices[i]).SizeSquared(); if (diffSqr < LOCAL_EPS * LOCAL_EPS) isPresent = 1; } if (!isPresent) vertices.Add(newVertex); } static bool PromptToRemoveExistingCollision(UStaticMesh* StaticMesh) { check(StaticMesh); UBodySetup* bs = StaticMesh->GetBodySetup(); if (bs && (bs->AggGeom.GetElementCount() > 0)) { // If we already have some simplified collision for this mesh - check before we clobber it. /*const EAppReturnType::Type ret = FMessageDialog::Open(EAppMsgType::YesNoCancel, NSLOCTEXT("UnrealEd", "StaticMeshAlreadyHasGeom", "Static Mesh already has simple collision.\nDo you want to replace it?")); if (ret == EAppReturnType::Yes) { bs->RemoveSimpleCollision(); } else if (ret == EAppReturnType::Cancel) { return false; }*/ } else { // Otherwise, create one here. StaticMesh->CreateBodySetup(); bs = StaticMesh->GetBodySetup(); } return true; } /* ******************************** KDOP ******************************** */ // This function takes the current collision model, and fits a k-DOP around it. // It uses the array of k unit-length direction vectors to define the k bounding planes. // THIS FUNCTION REPLACES EXISTING SIMPLE COLLISION MODEL WITH KDOP #define MY_FLTMAX (3.402823466e+38F) int32 GenerateKDopAsSimpleCollision(UStaticMesh* StaticMesh, const TArray& Dirs) { // Make sure rendering is done - so we are not changing data being used by collision drawing. FlushRenderingCommands(); if (!PromptToRemoveExistingCollision(StaticMesh)) { return INDEX_NONE; } UBodySetup* bs = StaticMesh->GetBodySetup(); // Do k- specific stuff. int32 kCount = Dirs.Num(); TArray maxDist; for (int32 i = 0; i < kCount; i++) maxDist.Add(-MY_FLTMAX); // Construct temporary UModel for kdop creation. We keep no refs to it, so it can be GC'd. auto TempModel = NewObject(); TempModel->Initialize(nullptr, 1); // For each vertex, project along each kdop direction, to find the max in that direction. const FStaticMeshLODResources& RenderData = StaticMesh->GetRenderData()->LODResources[0]; for (int32 i = 0; i < RenderData.GetNumVertices(); i++) { for (int32 j = 0; j < kCount; j++) { float dist = RenderData.VertexBuffers.PositionVertexBuffer.VertexPosition(i) | Dirs[j]; maxDist[j] = FMath::Max(dist, maxDist[j]); } } // Inflate kdop to ensure it is no degenerate const float MinSize = 0.1f; for (int32 i = 0; i < kCount; i++) { maxDist[i] += MinSize; } // Now we have the planes of the kdop, we work out the face polygons. TArray planes; for (int32 i = 0; i < kCount; i++) planes.Add(FPlane(Dirs[i], maxDist[i])); for (int32 i = 0; i < planes.Num(); i++) { FPoly* Polygon = new (TempModel->Polys->Element) FPoly(); FVector Base, AxisX, AxisY; Polygon->Init(); Polygon->Normal = planes[i]; Polygon->Normal.FindBestAxisVectors(AxisX, AxisY); Base = planes[i] * planes[i].W; new (Polygon->Vertices) FVector(Base + AxisX * HALF_WORLD_MAX + AxisY * HALF_WORLD_MAX); new (Polygon->Vertices) FVector(Base + AxisX * HALF_WORLD_MAX - AxisY * HALF_WORLD_MAX); new (Polygon->Vertices) FVector(Base - AxisX * HALF_WORLD_MAX - AxisY * HALF_WORLD_MAX); new (Polygon->Vertices) FVector(Base - AxisX * HALF_WORLD_MAX + AxisY * HALF_WORLD_MAX); for (int32 j = 0; j < planes.Num(); j++) { if (i != j) { if (!Polygon->Split(-FVector(planes[j]), planes[j] * planes[j].W)) { Polygon->Vertices.Empty(); break; } } } if (Polygon->Vertices.Num() < 3) { // If poly resulted in no verts, remove from array TempModel->Polys->Element.RemoveAt(TempModel->Polys->Element.Num() - 1); } else { // Other stuff... Polygon->iLink = i; Polygon->CalcNormal(1); } } if (TempModel->Polys->Element.Num() < 4) { TempModel = NULL; return INDEX_NONE; } // Build bounding box. TempModel->BuildBound(); // Build BSP for the brush. FBSPOps::bspBuild(TempModel, FBSPOps::BSP_Good, 15, 70, 1, 0); FBSPOps::bspRefresh(TempModel, 1); FBSPOps::bspBuildBounds(TempModel); bs->Modify(); bs->CreateFromModel(TempModel, false); // create all body instances RefreshCollisionChange(*StaticMesh); // Mark staticmesh as dirty, to help make sure it gets saved. StaticMesh->MarkPackageDirty(); return bs->AggGeom.ConvexElems.Num() - 1; } /* ******************************** BOX ******************************** */ void ComputeBoundingBox(UStaticMesh* StaticMesh, FVector& Center, FVector& Extents) { // Calculate bounding Box. FBox BoundingBox = StaticMesh->GetMeshDescription(0)->ComputeBoundingBox(); BoundingBox.GetCenterAndExtents(Center, Extents); } int32 GenerateBoxAsSimpleCollision(UStaticMesh* StaticMesh) { if (!PromptToRemoveExistingCollision(StaticMesh)) { return INDEX_NONE; } UBodySetup* bs = StaticMesh->GetBodySetup(); // Calculate bounding Box. FVector Center, Extents; StaticMesh->GetMeshDescription(0)->ComputeBoundingBox().GetCenterAndExtents(Center, Extents); Extents *= bs->BuildScale3D; bs->Modify(); // Create new GUID bs->InvalidatePhysicsData(); FKBoxElem BoxElem; BoxElem.Center = Center; BoxElem.X = Extents.X * 2.0f; BoxElem.Y = Extents.Y * 2.0f; BoxElem.Z = Extents.Z * 2.0f; bs->AggGeom.BoxElems.Add(BoxElem); // refresh collision change back to staticmesh components RefreshCollisionChange(*StaticMesh); // Mark staticmesh as dirty, to help make sure it gets saved. StaticMesh->MarkPackageDirty(); StaticMesh->bCustomizedCollision = true; // mark the static mesh for collision customization return bs->AggGeom.BoxElems.Num() - 1; } /* ******************************** SPHERE ******************************** */ // Can do bounding circles as well... Set elements of limitVect to 1.f for directions to consider, and 0.f to not consider. // Have 2 algorithms, seem better in different cirumstances // This algorithm taken from Ritter, 1990 // This one seems to do well with asymmetric input. static void CalcBoundingSphere(const FMeshDescription* MeshDescription, FSphere& sphere, FVector& LimitVec) { if (MeshDescription->Vertices().Num() == 0) return; FBox Box; FVector MinIx[3]; FVector MaxIx[3]; FStaticMeshConstAttributes Attributes(*MeshDescription); TVertexAttributesConstRef VertexPositions = Attributes.GetVertexPositions(); bool bFirstVertex = true; for (const FVertexID VertexID: MeshDescription->Vertices().GetElementIDs()) { FVector p = VertexPositions[VertexID] * LimitVec; if (bFirstVertex) { // First, find AABB, remembering furthest points in each dir. Box.Min = p; Box.Max = Box.Min; MinIx[0] = VertexPositions[VertexID]; MinIx[1] = VertexPositions[VertexID]; MinIx[2] = VertexPositions[VertexID]; MaxIx[0] = VertexPositions[VertexID]; MaxIx[1] = VertexPositions[VertexID]; MaxIx[2] = VertexPositions[VertexID]; bFirstVertex = false; continue; } // X // if (p.X < Box.Min.X) { Box.Min.X = p.X; MinIx[0] = VertexPositions[VertexID]; } else if (p.X > Box.Max.X) { Box.Max.X = p.X; MaxIx[0] = VertexPositions[VertexID]; } // Y // if (p.Y < Box.Min.Y) { Box.Min.Y = p.Y; MinIx[1] = VertexPositions[VertexID]; } else if (p.Y > Box.Max.Y) { Box.Max.Y = p.Y; MaxIx[1] = VertexPositions[VertexID]; } // Z // if (p.Z < Box.Min.Z) { Box.Min.Z = p.Z; MinIx[2] = VertexPositions[VertexID]; } else if (p.Z > Box.Max.Z) { Box.Max.Z = p.Z; MaxIx[2] = VertexPositions[VertexID]; } } const FVector Extremes[3] = {(MaxIx[0] - MinIx[0]) * LimitVec, (MaxIx[1] - MinIx[1]) * LimitVec, (MaxIx[2] - MinIx[2]) * LimitVec}; // Now find extreme points furthest apart, and initial center and radius of sphere. float d2 = 0.f; for (int32 i = 0; i < 3; i++) { const float tmpd2 = Extremes[i].SizeSquared(); if (tmpd2 > d2) { d2 = tmpd2; sphere.Center = (MinIx[i] + (0.5f * Extremes[i])) * LimitVec; sphere.W = 0.f; } } const FVector Extents = FVector(Extremes[0].X, Extremes[1].Y, Extremes[2].Z); // radius and radius squared float r = 0.5f * Extents.GetMax(); float r2 = FMath::Square(r); // Now check each point lies within this sphere. If not - expand it a bit. for (const FVertexID VertexID: MeshDescription->Vertices().GetElementIDs()) { const FVector cToP = (VertexPositions[VertexID] * LimitVec) - sphere.Center; const float pr2 = cToP.SizeSquared(); // If this point is outside our current bounding sphere's radius if (pr2 > r2) { // ..expand radius just enough to include this point. const float pr = FMath::Sqrt(pr2); r = 0.5f * (r + pr); r2 = FMath::Square(r); sphere.Center += ((pr - r) / pr * cToP); } } sphere.W = r; } // This is the one thats already used by unreal. // Seems to do better with more symmetric input... static void CalcBoundingSphere2(const FMeshDescription* MeshDescription, FSphere& sphere, FVector& LimitVec) { FVector Center = MeshDescription->ComputeBoundingBox().GetCenter(); sphere.Center = Center; sphere.W = 0.0f; FStaticMeshConstAttributes Attributes(*MeshDescription); TVertexAttributesConstRef VertexPositions = Attributes.GetVertexPositions(); for (const FVertexID VertexID: MeshDescription->Vertices().GetElementIDs()) { float Dist = FVector::DistSquared(VertexPositions[VertexID] * LimitVec, sphere.Center); if (Dist > sphere.W) sphere.W = Dist; } sphere.W = FMath::Sqrt(sphere.W); } // // // int32 GenerateSphereAsSimpleCollision(UStaticMesh* StaticMesh) { if (!PromptToRemoveExistingCollision(StaticMesh)) { return INDEX_NONE; } UBodySetup* bs = StaticMesh->GetBodySetup(); FSphere bSphere, bSphere2, bestSphere; FVector unitVec = bs->BuildScale3D; // Calculate bounding sphere. FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(0); check(MeshDescription); CalcBoundingSphere(MeshDescription, bSphere, unitVec); CalcBoundingSphere2(MeshDescription, bSphere2, unitVec); if (bSphere.W < bSphere2.W) bestSphere = bSphere; else bestSphere = bSphere2; // Dont use if radius is zero. if (bestSphere.W <= 0.f) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Prompt_10", "Could not create geometry.")); return INDEX_NONE; } bs->Modify(); // Create new GUID bs->InvalidatePhysicsData(); FKSphereElem SphereElem; SphereElem.Center = bestSphere.Center; SphereElem.Radius = bestSphere.W; bs->AggGeom.SphereElems.Add(SphereElem); // refresh collision change back to staticmesh components RefreshCollisionChange(*StaticMesh); // Mark staticmesh as dirty, to help make sure it gets saved. StaticMesh->MarkPackageDirty(); StaticMesh->bCustomizedCollision = true; // mark the static mesh for collision customization return bs->AggGeom.SphereElems.Num() - 1; } /* ******************************** SPHYL ******************************** */ static void CalcBoundingSphyl(const FMeshDescription* MeshDescription, FSphere& sphere, float& length, FRotator& rotation, FVector& LimitVec) { if (MeshDescription->Vertices().Num() == 0) return; FVector Center, Extents; MeshDescription->ComputeBoundingBox().GetCenterAndExtents(Center, Extents); Extents *= LimitVec; // @todo sphere.Center could perhaps be adjusted to best fit if model is non-symmetric on it's longest axis sphere.Center = Center; // Work out best axis aligned orientation (longest side) float Extent = Extents.GetMax(); if (Extent == Extents.X) { rotation = FRotator(90.f, 0.f, 0.f); Extents.X = 0.0f; } else if (Extent == Extents.Y) { rotation = FRotator(0.f, 0.f, 90.f); Extents.Y = 0.0f; } else { rotation = FRotator(0.f, 0.f, 0.f); Extents.Z = 0.0f; } // Cleared the largest axis above, remaining determines the radius float r = Extents.GetMax(); float r2 = FMath::Square(r); FStaticMeshConstAttributes Attributes(*MeshDescription); TVertexAttributesConstRef VertexPositions = Attributes.GetVertexPositions(); // Now check each point lies within this the radius. If not - expand it a bit. for (const FVertexID VertexID: MeshDescription->Vertices().GetElementIDs()) { FVector cToP = (VertexPositions[VertexID] * LimitVec) - sphere.Center; cToP = rotation.UnrotateVector(cToP); const float pr2 = cToP.SizeSquared2D(); // Ignore Z here... // If this point is outside our current bounding sphere's radius if (pr2 > r2) { // ..expand radius just enough to include this point. const float pr = FMath::Sqrt(pr2); r = 0.5f * (r + pr); r2 = FMath::Square(r); } } // The length is the longest side minus the radius. float hl = FMath::Max(0.0f, Extent - r); // Now check each point lies within the length. If not - expand it a bit. for (const FVertexID VertexID: MeshDescription->Vertices().GetElementIDs()) { FVector cToP = (VertexPositions[VertexID] * LimitVec) - sphere.Center; cToP = rotation.UnrotateVector(cToP); // If this point is outside our current bounding sphyl's length if (FMath::Abs(cToP.Z) > hl) { const bool bFlip = (cToP.Z < 0.f ? true : false); const FVector cOrigin(0.f, 0.f, (bFlip ? -hl : hl)); const float pr2 = (cOrigin - cToP).SizeSquared(); // If this point is outside our current bounding sphyl's radius if (pr2 > r2) { FVector cPoint; FMath::SphereDistToLine(cOrigin, r, cToP, (bFlip ? FVector(0.f, 0.f, 1.f) : FVector(0.f, 0.f, -1.f)), cPoint); // Don't accept zero as a valid diff when we know it's outside the sphere (saves needless retest on further iterations of like points) hl += FMath::Max(FMath::Abs(cToP.Z - cPoint.Z), 1.e-6f); } } } sphere.W = r; length = hl * 2.0f; } // // // int32 GenerateSphylAsSimpleCollision(UStaticMesh* StaticMesh) { if (!PromptToRemoveExistingCollision(StaticMesh)) { return INDEX_NONE; } UBodySetup* bs = StaticMesh->GetBodySetup(); FSphere sphere; float length; FRotator rotation; FVector unitVec = bs->BuildScale3D; // Calculate bounding box. FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(0); check(MeshDescription); CalcBoundingSphyl(MeshDescription, sphere, length, rotation, unitVec); // Dont use if radius is zero. if (sphere.W <= 0.f) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Prompt_10", "Could not create geometry.")); return INDEX_NONE; } // If height is zero, then a sphere would be better (should we just create one instead?) if (length <= 0.f) { length = SMALL_NUMBER; } bs->Modify(); // Create new GUID bs->InvalidatePhysicsData(); FKSphylElem SphylElem; SphylElem.Center = sphere.Center; SphylElem.Rotation = rotation; SphylElem.Radius = sphere.W; SphylElem.Length = length; bs->AggGeom.SphylElems.Add(SphylElem); // refresh collision change back to staticmesh components RefreshCollisionChange(*StaticMesh); // Mark staticmesh as dirty, to help make sure it gets saved. StaticMesh->MarkPackageDirty(); StaticMesh->bCustomizedCollision = true; // mark the static mesh for collision customization return bs->AggGeom.SphylElems.Num() - 1; } void RefreshCollisionChange(UStaticMesh& StaticMesh) { TRACE_CPUPROFILER_EVENT_SCOPE(RefreshCollisionChange) StaticMesh.CreateNavCollision(/*bIsUpdate=*/true); for (FThreadSafeObjectIterator Iter(UStaticMeshComponent::StaticClass()); Iter; ++Iter) { UStaticMeshComponent* StaticMeshComponent = Cast(*Iter); if (StaticMeshComponent->GetStaticMesh() == &StaticMesh) { // it needs to recreate IF it already has been created if (StaticMeshComponent->IsPhysicsStateCreated()) { StaticMeshComponent->RecreatePhysicsState(); } } } FEditorSupportDelegates::RedrawAllViewports.Broadcast(); } void RefreshCollisionChanges(const TArray& StaticMeshes) { TRACE_CPUPROFILER_EVENT_SCOPE(RefreshCollisionChanges) for (UStaticMesh* StaticMesh: StaticMeshes) { StaticMesh->CreateNavCollision(/*bIsUpdate=*/true); } for (FThreadSafeObjectIterator Iter(UStaticMeshComponent::StaticClass()); Iter; ++Iter) { UStaticMeshComponent* StaticMeshComponent = Cast(*Iter); if (StaticMeshes.Contains(StaticMeshComponent->GetStaticMesh())) { // it needs to recreate IF it already has been created if (StaticMeshComponent->IsPhysicsStateCreated()) { StaticMeshComponent->RecreatePhysicsState(); } } } FEditorSupportDelegates::RedrawAllViewports.Broadcast(); } /* *************************** DEPRECATED ******************************** */ void RefreshCollisionChange(const UStaticMesh* StaticMesh) { if (StaticMesh) { RefreshCollisionChange(const_cast(*StaticMesh)); } }