Files
UnrealEngine/Engine/Plugins/Experimental/PlanarCutPlugin/Source/PlanarCut/Private/FractureAutoUV.cpp
2025-05-18 13:04:45 +08:00

1192 lines
38 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "FractureAutoUV.h"
#include "PlanarCutPlugin.h"
#include "Distance/DistLine3Segment3.h"
#include "GeometryMeshConversion.h"
#include "Distance/DistLine3Triangle3.h"
#include "GeometryCollection/GeometryCollectionAlgo.h"
#include "Distance/DistSegment3Triangle3.h"
#include "DynamicMesh/MeshNormals.h"
#include "Generators/MeshShapeGenerator.h"
#include "Parameterization/DynamicMeshUVEditor.h"
#include "Sampling/MeshImageBakingCache.h"
#include "DynamicMesh/Operations/MergeCoincidentMeshEdges.h"
#include "Implicit/Solidify.h"
#include "Parameterization/PatchBasedMeshUVGenerator.h"
#include "Parameterization/UVPacking.h"
#include "Sampling/MeshCurvatureMapBaker.h"
#include "Sampling/MeshOcclusionMapBaker.h"
#include "Solvers/MeshSmoothing.h"
#include "DisjointSet.h"
#include "Image/ImageOccupancyMap.h"
#include "Spatial/FastWinding.h"
#include "Util/ProgressCancel.h"
#if WITH_EDITOR
#endif
using namespace UE::Geometry;
namespace UE
{
namespace UVPacking
{
using namespace UE::Geometry;
/**
* Create UV islands from a triangle mesh connectivity.
* Assumes the triangles are already split at UV seams, but topologically connected otherwise.
* Not recommended for meshes that already have built-in edge connectivity data.
*
* @param Mesh The mesh to create islands for
* @param IslandsOut The triangle IDs for each island
* @param IncludeTri Optional function to filter which tris are assigned to islands
*/
template <typename TriangleMeshType>
inline void CreateUVIslandsFromMeshTopology(TriangleMeshType& Mesh,
TArray<TArray<int>>& IslandsOut,
TFunctionRef<bool(int32)> IncludeTri = [](int32)
{
return true;
})
{
FDisjointSet VertComponents(Mesh.MaxVertexID());
// Add Source vertices to hash & disjoint sets
for (int32 TID = 0; TID < Mesh.MaxTriangleID(); TID++)
{
if (!Mesh.IsTriangle(TID) || !IncludeTri(TID))
{
continue;
}
FIndex3i Tri = Mesh.GetTriangle(TID);
for (int32 First = 2, Second = 0; Second < 3; First = Second++)
{
VertComponents.Union(Tri[First], Tri[Second]);
}
}
IslandsOut.Reset();
TMap<uint32, int32> IslandIDToIdx;
for (int32 TID = 0; TID < Mesh.MaxTriangleID(); TID++)
{
if (!Mesh.IsTriangle(TID) || !IncludeTri(TID))
{
continue;
}
FIndex3i Tri = Mesh.GetTriangle(TID);
uint32 IslandID = VertComponents.Find(Tri.A);
int32 Idx = -1;
int32* FoundIdx = IslandIDToIdx.Find(IslandID);
if (!FoundIdx)
{
Idx = IslandsOut.Emplace();
IslandIDToIdx.Add(IslandID, Idx);
}
else
{
Idx = *FoundIdx;
}
IslandsOut[Idx].Add(TID);
}
}
}
} // namespace UE::UVPacking
namespace UE::PrivateUVHelpers
{
// Helper to update geometry UVs for meshes in specified bones via some processing that operates on a dynamic mesh
bool ApplyDynamicMeshBasedUVProcessing(
int32 TargetUVLayer, FGeometryCollection& Collection,
TArrayView<const int32> TransformIndices, TFunctionRef<bool(FDynamicMesh3&, int32 TransformIndex)> ProcessMesh,
bool bWeldUVs = true, bool bSaveIsolatedVertices = true, bool bNeedsSplitAttributes = true)
{
using namespace UE::PlanarCut;
int32 NumUVLayers = Collection.NumUVLayers();
if (TargetUVLayer < 0 || TargetUVLayer >= NumUVLayers)
{
return false;
}
FDynamicMeshCollection CollectionMeshes(&Collection, TransformIndices, FTransform::Identity, bSaveIsolatedVertices);
bool bAnyUpdate = false;
for (int MeshIdx = 0; MeshIdx < CollectionMeshes.Meshes.Num(); MeshIdx++)
{
int32 TransformIdx = CollectionMeshes.Meshes[MeshIdx].TransformIndex;
FDynamicMesh3& Mesh = CollectionMeshes.Meshes[MeshIdx].AugMesh;
FMeshNormals::InitializeOverlayToPerVertexNormals(Mesh.Attributes()->PrimaryNormals(), true);
AugmentedDynamicMesh::InitializeOverlayToPerVertexTangents(Mesh);
AugmentedDynamicMesh::InitializeOverlayToPerVertexUVs(Mesh, NumUVLayers, 0);
// Optionally weld edges so we won't have artificial UV seams on sharp edges, etc
if (bWeldUVs)
{
FMergeCoincidentMeshEdges EdgeWelder(&Mesh);
EdgeWelder.Apply();
}
if (ProcessMesh(Mesh, TransformIdx))
{
bAnyUpdate = true;
Mesh.CompactInPlace();
// Geometry collection requires split attributes, so we always need to re-split them if we've welded above.
// We also re-split them if requested (i.e., if not welding, set this if ProcessMesh may create un-split vertices)
if (bWeldUVs || bNeedsSplitAttributes)
{
AugmentedDynamicMesh::SplitOverlayAttributesToPerVertex(Mesh, true, true);
}
}
}
if (bAnyUpdate)
{
CollectionMeshes.UpdateAllCollections(Collection);
}
return bAnyUpdate;
}
}
namespace UE { namespace PlanarCut {
namespace {
inline bool IsTriActive(bool bIsVisible, bool bIsInternal, int32 MaterialID, const TSet<int32>& InsideMaterials, bool bActivateInsideTriangles, ETargetFaces TargetFaces)
{
if (!bIsVisible) // never select invisible triangles
{
return false;
}
if (TargetFaces == ETargetFaces::AllFaces && bActivateInsideTriangles)
{
return true;
}
if (
(TargetFaces == ETargetFaces::InternalFaces && (bIsInternal == bActivateInsideTriangles)) ||
(TargetFaces == ETargetFaces::ExternalFaces && (bIsInternal != bActivateInsideTriangles))
)
{
return true;
}
if (InsideMaterials.Num() > 0 && (InsideMaterials.Contains(MaterialID) == bActivateInsideTriangles))
{
return true;
}
return false;
}
}
/**
* Shared helper method to set 'active' subset of triangles for a collection, to be considered for texturing
* @return number of active triangles
*/
int32 SetActiveTriangles(const FGeometryCollection* Collection, TArray<bool>& ActiveTrianglesOut,
bool bActivateInsideTriangles, ETargetFaces TargetFaces, TArrayView<int32> WhichMaterialsAreInside)
{
TSet<int32> InsideMaterials;
for (int32 MatID : WhichMaterialsAreInside)
{
InsideMaterials.Add(MatID);
}
ActiveTrianglesOut.Init(false, Collection->Indices.Num());
int32 NumTriangles = 0;
for (int TID = 0; TID < Collection->Indices.Num(); TID++)
{
bool bIsActive = IsTriActive(Collection->Visible[TID], Collection->Internal[TID], Collection->MaterialID[TID], InsideMaterials, bActivateInsideTriangles, TargetFaces);
if (bIsActive)
{
ActiveTrianglesOut[TID] = true;
NumTriangles++;
}
}
return NumTriangles;
}
// Adapter to use geometry collections in the UVPacker, and other GeometricObjects generic mesh processing code
// Note: Adapts the whole geometry collection as a single mesh, with triangles filtered by an ActiveTriangles mask
struct FGeomMesh : public UE::Geometry::FUVPacker::IUVMeshView
{
FGeometryCollection* Collection;
const TArrayView<bool> ActiveTriangles;
int32 NumTriangles;
TArray<FVector3d> GlobalVertices; // vertices transformed to global space
TArray<FVector3f> GlobalNormals; // normals transformed to global space
int32 UVLayer = 0;
TManagedArray<FVector2f>* CollectionUVs;
// Construct a mesh from a geometry collection and a mask of active triangles
FGeomMesh(int32 UVLayer, FGeometryCollection* Collection, TArrayView<bool> ActiveTriangles, int32 NumTriangles)
: Collection(Collection), ActiveTriangles(ActiveTriangles), NumTriangles(NumTriangles), UVLayer(UVLayer),
CollectionUVs(Collection->FindUVLayer(UVLayer))
{
ValidateUVLayer();
InitVertices();
}
// Construct a mesh from an existing FGeomMesh and a mask of active triangles
FGeomMesh(const FGeomMesh& OtherMesh, TArrayView<bool> ActiveTriangles, int32 NumTriangles)
: Collection(OtherMesh.Collection), ActiveTriangles(ActiveTriangles), NumTriangles(NumTriangles),
UVLayer(OtherMesh.UVLayer), CollectionUVs(OtherMesh.Collection->FindUVLayer(OtherMesh.UVLayer))
{
ValidateUVLayer();
GlobalVertices = OtherMesh.GlobalVertices;
}
void ValidateUVLayer()
{
if (!ensure(UVLayer >= 0 && UVLayer < Collection->NumUVLayers()))
{
UVLayer = FMath::Clamp(UVLayer, 0, Collection->NumUVLayers() - 1);
CollectionUVs = Collection->FindUVLayer(UVLayer);
}
check(CollectionUVs);
}
void InitVertices()
{
// transform all vertices from local to global space
TArray<FTransform> GlobalTransformArray;
GeometryCollectionAlgo::GlobalMatrices(Collection->Transform, Collection->Parent, GlobalTransformArray);
GlobalVertices.SetNum(Collection->Vertex.Num());
GlobalNormals.SetNum(Collection->Vertex.Num());
for (int32 Idx = 0; Idx < GlobalVertices.Num(); Idx++)
{
GlobalNormals[Idx] = (FVector3f)GlobalTransformArray[Collection->BoneMap[Idx]].TransformVectorNoScale(FVector(Collection->Normal[Idx]));
GlobalVertices[Idx] = (FVector3d)GlobalTransformArray[Collection->BoneMap[Idx]].TransformPosition(FVector(Collection->Vertex[Idx]));
}
}
///
/// The FUVPacker::IUVMeshView interface functions; this is the subset of functionality required for FUVPacker
///
virtual FIndex3i GetTriangle(int32 TID) const
{
return FIndex3i(Collection->Indices[TID]);
}
virtual FIndex3i GetUVTriangle(int32 TID) const
{
return FIndex3i(Collection->Indices[TID]);
}
virtual FVector3d GetVertex(int32 VID) const
{
return GlobalVertices[VID];
}
virtual FVector2f GetUV(int32 VID) const
{
return (*CollectionUVs)[VID];
}
virtual void SetUV(int32 VID, FVector2f UVIn)
{
FVector2f& UV = (*CollectionUVs)[VID];
UV.X = UVIn.X;
UV.Y = UVIn.Y;
}
///
/// Interface for templated mesh code, not part of the the IUVMeshView interface
///
inline bool IsTriangle(int32 TID) const
{
return ActiveTriangles[TID];
}
constexpr inline bool IsVertex(int32 VID) const
{
return true; // we don't have sparse vertices, so can always return true
}
inline int32 MaxTriangleID() const
{
return Collection->Indices.Num();
}
inline int32 MaxVertexID() const
{
return GlobalVertices.Num();
}
inline int32 TriangleCount() const
{
return NumTriangles;
}
inline int32 VertexCount() const
{
return GlobalVertices.Num();
}
constexpr inline uint64 GetChangeStamp() const
{
return 1;
}
inline void GetTriVertices(int TID, UE::Math::TVector<double>& V0, UE::Math::TVector<double>& V1, UE::Math::TVector<double>& V2) const
{
FIntVector TriRaw = Collection->Indices[TID];
V0 = GlobalVertices[TriRaw.X];
V1 = GlobalVertices[TriRaw.Y];
V2 = GlobalVertices[TriRaw.Z];
}
/// End of interfaces
FVector3f GetInterpolatedNormal(int TID, const FVector3d& Bary)
{
FIntVector TriRaw = Collection->Indices[TID];
FVector3f Normal = (FVector3f) (Bary.X * GlobalNormals[TriRaw.X] + Bary.Y * GlobalNormals[TriRaw.Y] + Bary.Z * GlobalNormals[TriRaw.Z]);
return Normalized(Normal);
}
};
/// Like FGeomMesh, but the UVs are given as the vertices rather than accessed separately
/// Used by the texture sampling code
struct FGeomFlatUVMesh
{
FGeometryCollection* Collection;
const TArrayView<bool> ActiveTriangles;
int32 NumTriangles;
int32 UVLayer;
TManagedArray<FVector2f>* CollectionUVs;
FGeomFlatUVMesh(int32 UVLayer, FGeometryCollection* Collection, TArrayView<bool> ActiveTriangles, int32 NumTriangles)
: Collection(Collection), ActiveTriangles(ActiveTriangles), NumTriangles(NumTriangles), UVLayer(UVLayer), CollectionUVs(Collection->FindUVLayer(UVLayer))
{
ValidateUVLayer();
}
void ValidateUVLayer()
{
if (!ensure(UVLayer >= 0 && UVLayer < Collection->NumUVLayers()))
{
UVLayer = FMath::Clamp(UVLayer, 0, Collection->NumUVLayers());
CollectionUVs = Collection->FindUVLayer(UVLayer);
}
check(CollectionUVs);
}
inline FIndex3i GetTriangle(int32 TID) const
{
return FIndex3i(Collection->Indices[TID]);
}
inline FVector3d GetVertex(int32 VID) const
{
const FVector2f& UV = (*CollectionUVs)[VID];
return FVector3d(UV.X, UV.Y, 0);
}
inline bool IsTriangle(int32 TID) const
{
return ActiveTriangles[TID];
}
constexpr inline bool IsVertex(int32 VID) const
{
return true; // we don't have sparse vertices, so can always return true
}
inline int32 MaxTriangleID() const
{
return Collection->Indices.Num();
}
inline int32 MaxVertexID() const
{
return (*CollectionUVs).Num();
}
inline int32 TriangleCount() const
{
return NumTriangles;
}
inline int32 VertexCount() const
{
return (*CollectionUVs).Num();
}
constexpr inline uint64 GetChangeStamp() const
{
return 1;
}
inline void GetTriVertices(int TID, UE::Math::TVector<double>& V0, UE::Math::TVector<double>& V1, UE::Math::TVector<double>& V2) const
{
FIntVector TriRaw = Collection->Indices[TID];
V0 = GetVertex(TriRaw.X);
V1 = GetVertex(TriRaw.Y);
V2 = GetVertex(TriRaw.Z);
}
};
bool MergeUVIslands(
int32 TargetUVLayer,
FGeometryCollection& Collection,
FMergeIslandSettings MergeIslandSettings,
TArrayView<bool> FaceSelection,
FProgressCancel* Progress
)
{
TSet<int32> ActiveTransformIndexSet;
for (int32 FaceIdx = 0; FaceIdx < FaceSelection.Num(); ++FaceIdx)
{
ActiveTransformIndexSet.Add(Collection.BoneMap[Collection.Indices[FaceIdx].X]);
}
TArray<int32> TransformIndices = ActiveTransformIndexSet.Array();
return UE::PrivateUVHelpers::ApplyDynamicMeshBasedUVProcessing(
TargetUVLayer, Collection,
TransformIndices, [&MergeIslandSettings, &FaceSelection, &Collection, TargetUVLayer](FDynamicMesh3& Mesh, int32 TransformIndex) -> bool
{
FDynamicMeshUVOverlay* UVLayer = Mesh.Attributes()->GetUVLayer(TargetUVLayer);
if (!ensure(UVLayer))
{
return false;
}
// Pull the selected faces for the current mesh from the overall FaceSelection, via the mesh TransformIndex
int32 GeometryIndex = Collection.TransformToGeometryIndex[TransformIndex];
int32 FaceStart = Collection.FaceStart[GeometryIndex];
// Note: Mesh must have the same triangle count as the corresponding geometry, and the triangles must be compact
if (!ensure(Collection.FaceCount[GeometryIndex] == Mesh.TriangleCount() && Mesh.IsCompactT()))
{
return false;
}
TArray<int32> TargetTris;
for (int TID : Mesh.TriangleIndicesItr())
{
if (FaceSelection[TID + FaceStart])
{
TargetTris.Add(TID);
}
}
if (TargetTris.IsEmpty())
{
return false;
}
FPatchBasedMeshUVGenerator PatchUVGen;
PatchUVGen.MergingThreshold = MergeIslandSettings.AreaDistortionThreshold;
PatchUVGen.MaxNormalDeviationDeg = MergeIslandSettings.MaxNormalDeviationDeg;
PatchUVGen.NormalSmoothingAlpha = MergeIslandSettings.NormalSmoothingAlpha;
PatchUVGen.NormalSmoothingRounds = MergeIslandSettings.NormalSmoothingRounds;
FMeshConnectedComponents InitialComponents(&Mesh);
InitialComponents.FindConnectedTriangles(TargetTris, [&Mesh, &UVLayer](int32 TA, int32 TB) -> bool
{
return UVLayer->AreTrianglesConnected(TA, TB);
}
);
TArray<TArray<int32>> MergedIslands;
PatchUVGen.ComputeIslandsByRegionMerging(Mesh, *UVLayer, InitialComponents, true, MergedIslands);
if (MergedIslands.IsEmpty())
{
// nothing merged, no need update
return false;
}
TArray<double> OriginalUVAreas;
TArray<FVector2f> RefUVPos;
OriginalUVAreas.SetNumZeroed(MergedIslands.Num());
RefUVPos.SetNumZeroed(MergedIslands.Num());
for (int32 IslandIdx = 0; IslandIdx < MergedIslands.Num(); ++IslandIdx)
{
OriginalUVAreas[IslandIdx] = FDynamicMeshUVEditor::DetermineAreaFromUVs(*UVLayer, MergedIslands[IslandIdx]);
// Find any valid UV as a reference position
for (int32 TID : MergedIslands[IslandIdx])
{
FIndex3i ElTri = UVLayer->GetTriangle(TID);
if (ElTri.A != INDEX_NONE)
{
RefUVPos[IslandIdx] = UVLayer->GetElement(ElTri.A);
break;
}
}
}
TArray<bool> ValidUVsComputed;
int32 NumSolvesFailed = PatchUVGen.ComputeUVsFromTriangleSets(Mesh, *UVLayer, MergedIslands, ValidUVsComputed);
for (int32 IslandIdx = 0; IslandIdx < MergedIslands.Num(); ++IslandIdx)
{
double NewAreaSum = FDynamicMeshUVEditor::DetermineAreaFromUVs(*UVLayer, MergedIslands[IslandIdx]);
if (OriginalUVAreas[IslandIdx] > 0 && NewAreaSum > 0)
{
float Scale = (float)FMath::Sqrt(OriginalUVAreas[IslandIdx] / NewAreaSum);
FVector2f Origin = RefUVPos[IslandIdx];
FDynamicMeshUVEditor::TransformTriangleSelectionUVs(*UVLayer, MergedIslands[IslandIdx], [Scale, Origin](const FVector2f& UV)
{
return (UV - Origin) * Scale + Origin;
});
}
}
return true;
},
true
);
}
bool BoxProjectUVs(
int32 TargetUVLayer,
FGeometryCollection& Collection,
const FVector3d& BoxDimensions,
ETargetFaces TargetFaces,
TArrayView<int32> WhichMaterials,
FVector2f OffsetUVs,
bool bOverrideBoxDimensionsWithBounds,
bool bCenterBoxAtPivot,
bool bUniformProjectionScale
)
{
TArray<int32> TransformIndices;
for (int32 GeomIdx = 0; GeomIdx < Collection.TransformIndex.Num(); GeomIdx++)
{
TransformIndices.Add(Collection.TransformIndex[GeomIdx]);
}
TSet<int32> TargetMaterials;
TargetMaterials.Append(WhichMaterials);
return UE::PrivateUVHelpers::ApplyDynamicMeshBasedUVProcessing(
TargetUVLayer, Collection,
TransformIndices,
[&TargetMaterials, TargetUVLayer, &BoxDimensions, TargetFaces, &WhichMaterials, &OffsetUVs,
bOverrideBoxDimensionsWithBounds, bCenterBoxAtPivot, bUniformProjectionScale]
(FDynamicMesh3& Mesh, int32 TransformIndex) -> bool
{
TArray<int32> TargetTris;
for (int TID : Mesh.TriangleIndicesItr())
{
bool bIsTextureTri = IsTriActive(
AugmentedDynamicMesh::GetVisibility(Mesh, TID), AugmentedDynamicMesh::GetInternal(Mesh, TID),
Mesh.Attributes()->GetMaterialID()->GetValue(TID), TargetMaterials, true, TargetFaces);
if (bIsTextureTri)
{
TargetTris.Add(TID);
}
}
if (TargetTris.IsEmpty())
{
return false;
}
FDynamicMeshUVEditor UVEd(&Mesh, TargetUVLayer, false);
FFrame3d BoxFrame; // defaults to origin / no rotation
FVector3d UseBoxDimensions = bUniformProjectionScale ? FVector3d(BoxDimensions.X) : BoxDimensions;
if (!bCenterBoxAtPivot || bOverrideBoxDimensionsWithBounds)
{
FAxisAlignedBox3d Bounds = Mesh.GetBounds(true);
if (!bCenterBoxAtPivot)
{
BoxFrame.Origin = Bounds.Center();
}
if (bOverrideBoxDimensionsWithBounds)
{
UseBoxDimensions = bUniformProjectionScale ? FVector3d(Bounds.MaxDim()) : Bounds.Max - Bounds.Min;
}
}
UVEd.SetTriangleUVsFromBoxProjection(TargetTris, [](const FVector3d& Pos) { return Pos; }, BoxFrame, UseBoxDimensions, 1);
// apply offset to UV space
FDynamicMeshUVOverlay* Overlap = UVEd.GetOverlay();
for (int ElemID : Overlap->ElementIndicesItr())
{
Overlap->SetElement(ElemID, OffsetUVs + Overlap->GetElement(ElemID));
}
return true;
},
true);
}
bool UVLayout(
int32 TargetUVLayer,
FGeometryCollection& Collection,
int32 UVRes,
float GutterSize,
ETargetFaces TargetFaces,
TArrayView<int32> WhichMaterials,
bool bRecreateUVsForDegenerateIslands,
FProgressCancel* Progress
)
{
TArray<bool> FaceSelection;
int32 NumActive = SetActiveTriangles(&Collection, FaceSelection, true, TargetFaces, WhichMaterials);
return UVLayout(TargetUVLayer, Collection, UVRes, GutterSize, FaceSelection, bRecreateUVsForDegenerateIslands, Progress);
}
bool PLANARCUT_API UVLayout(
int32 TargetUVLayer,
FGeometryCollection& Collection,
int32 UVRes,
float GutterSize,
TArrayView<bool> FaceSelection,
bool bRecreateUVsForDegenerateIslands,
FProgressCancel* Progress
)
{
// TargetUVLayer must be valid for the collection
if (TargetUVLayer < 0 || TargetUVLayer >= GeometryCollectionUV::MAX_NUM_UV_CHANNELS || !Collection.FindUVLayer(TargetUVLayer))
{
UE_LOG(LogPlanarCut, Error, TEXT("UV Layout failed: Target UV Channel (%d) not found on collection"), TargetUVLayer);
return false;
}
// Face selection array size must match the collection face count
if (Collection.Indices.Num() != FaceSelection.Num())
{
UE_LOG(LogPlanarCut, Error, TEXT("UV Layout failed: Mismatch between collection face count (%d) and selection face count (%d)"), Collection.Indices.Num(), FaceSelection.Num());
return false;
}
FProgressCancel::FProgressScope InitScope = FProgressCancel::CreateScopeTo(Progress, .2);
int32 NumActive = 0;
for (const bool& bSelected : FaceSelection)
{
NumActive += (bSelected) ? 1 : 0;
}
FGeomMesh UVMesh(TargetUVLayer, &Collection, FaceSelection, NumActive);
TArray<TArray<int32>> UVIslands;
UE::UVPacking::CreateUVIslandsFromMeshTopology<FGeomMesh>(UVMesh, UVIslands);
InitScope.Done();
if (Progress && Progress->Cancelled())
{
return false;
}
if (bRecreateUVsForDegenerateIslands)
{
FProgressCancel::FProgressScope ForDegenerateIslandsScope = FProgressCancel::CreateScopeTo(Progress, .5);
TArray<int> IslandVert; IslandVert.SetNumUninitialized(UVMesh.MaxVertexID());
const int32 NumIslands = UVIslands.Num();
for (int IslandIdx = 0; IslandIdx < NumIslands; IslandIdx++)
{
double UVArea = 0;
FVector3d AvgNormal(0, 0, 0);
FVector3d AnyPt(0, 0, 0);
for (int TID : UVIslands[IslandIdx])
{
FIndex3i UVInds = UVMesh.GetUVTriangle(TID);
FVector2f UVA = UVMesh.GetUV(UVInds.A), UVB = UVMesh.GetUV(UVInds.B), UVC = UVMesh.GetUV(UVInds.C);
UVArea += (double)VectorUtil::Area(UVA, UVB, UVC);
FVector3d VA, VB, VC;
UVMesh.GetTriVertices(TID, VA, VB, VC);
double Area;
AnyPt = VA;
FVector3d Normal = VectorUtil::NormalArea(VA, VB, VC, Area);
AvgNormal += Area * Normal;
}
double AvgNormalLen = Normalize(AvgNormal);
bool bHasValidNormal = AvgNormalLen > 0;
bool bDoProjection = FMathd::Abs(UVArea) < FMathd::ZeroTolerance;
if (bDoProjection)
{
// convert the island to a dynamic mesh so we can use the expmap UVs
for (int VID = 0; VID < IslandVert.Num(); ++VID)
{
IslandVert[VID] = -1;
}
TArray<int> InvIslandVert; InvIslandVert.Reserve(3 * UVIslands[IslandIdx].Num());
FDynamicMesh3 IslandMesh;
for (int TID : UVIslands[IslandIdx])
{
FIndex3i Tri = UVMesh.GetUVTriangle(TID);
FIndex3i NewTri = FIndex3i::Invalid();
for (int SubIdx = 0; SubIdx < 3; SubIdx++)
{
int SrcVID = Tri[SubIdx];
int& VID = IslandVert[SrcVID];
if (VID == -1)
{
VID = IslandMesh.AppendVertex(UVMesh.GetVertex(SrcVID));
check(InvIslandVert.Num() == VID);
InvIslandVert.Add(SrcVID);
}
NewTri[SubIdx] = VID;
}
IslandMesh.AppendTriangle(NewTri);
}
FDynamicMeshUVEditor UVEditor(&IslandMesh, 0, true);
TArray<int> Tris; Tris.Reserve(IslandMesh.TriangleCount());
for (int TID : IslandMesh.TriangleIndicesItr())
{
Tris.Add(TID);
}
bool bExpMapSuccess = UVEditor.SetTriangleUVsFromExpMap(Tris);
if (bExpMapSuccess)
{
// transfer UVs from the overlay
FDynamicMeshUVOverlay* UVOverlay = IslandMesh.Attributes()->PrimaryUV();
for (int IslandTID : IslandMesh.TriangleIndicesItr())
{
FIndex3i Tri = IslandMesh.GetTriangle(IslandTID);
FIndex3i UVTri = UVOverlay->GetTriangle(IslandTID);
if (!ensure(UVTri != FIndex3i::Invalid()))
{
// we shouldn't have unset tris like this if the ExpMap returned success
bExpMapSuccess = false;
break;
}
for (int SubIdx = 0; SubIdx < 3; SubIdx++)
{
int VID = InvIslandVert[Tri[SubIdx]];
FVector2f UV = UVOverlay->GetElement(UVTri[SubIdx]);
UVMesh.SetUV(VID, UV);
}
}
}
if (!bExpMapSuccess && bHasValidNormal)
{
// expmap failed; fall back to projecting UVs
FFrame3d Frame(AnyPt, AvgNormal);
for (int TID : UVIslands[IslandIdx])
{
FIndex3i Tri = UVMesh.GetUVTriangle(TID);
for (int SubIdx = 0; SubIdx < 3; SubIdx++)
{
int VID = Tri[SubIdx];
FVector2f UV = (FVector2f)Frame.ToPlaneUV(UVMesh.GetVertex(VID));
UVMesh.SetUV(VID, UV);
}
}
}
}
}
}
if (Progress && Progress->Cancelled())
{
return false;
}
FProgressCancel::FProgressScope FinalScope = FProgressCancel::CreateScopeTo(Progress, 1);
UE::Geometry::FUVPacker Packer;
Packer.bScaleIslandsByWorldSpaceTexelRatio = true; // let packer scale islands separately to have consistent texel-to-world ratio
Packer.bAllowFlips = false;
// StandardPack doesn't support the Packer.GutterSize feature, and always tries to leave a 1 texel gutter.
// To approximate a larger gutter, we tell it to consider a smaller output resolution --
// hoping that e.g. the UVs for a 1 pixel gutter at 256x256 are ~ a 2 pixel gutter at 512x512
// TODO: If we make StandardPack support the GutterSize parameter, we can use that instead.
Packer.TextureResolution = static_cast<int32>( UVRes / FMathf::Max(1.f, GutterSize) );
return Packer.StandardPack(&UVMesh, UVIslands);
}
bool TextureInternalSurfaces(
int32 TargetUVLayer,
FGeometryCollection& Collection,
int32 GutterSize,
UE::Geometry::FIndex4i BakeAttributes,
const FTextureAttributeSettings& AttributeSettings,
UE::Geometry::TImageBuilder<FVector4f>& TextureOut,
EUseMaterials MaterialsPattern,
TArrayView<int32> WhichMaterials,
FProgressCancel* Progress
)
{
ETargetFaces TargetFaces =
MaterialsPattern == EUseMaterials::AllMaterials ? ETargetFaces::AllFaces :
MaterialsPattern == EUseMaterials::OddMaterials ? ETargetFaces::InternalFaces : ETargetFaces::CustomFaces;
return TextureSpecifiedFaces(TargetUVLayer, Collection, GutterSize, BakeAttributes, AttributeSettings, TextureOut, TargetFaces, WhichMaterials, Progress);
}
bool TextureSpecifiedFaces(
int32 TargetUVLayer,
FGeometryCollection& Collection,
int32 GutterSize,
FIndex4i BakeAttributes,
const FTextureAttributeSettings& AttributeSettings,
TImageBuilder<FVector4f>& TextureOut,
ETargetFaces TargetFaces,
TArrayView<int32> WhichMaterials,
FProgressCancel* Progress
)
{
TArray<bool> ToTextureTriangles;
SetActiveTriangles(&Collection, ToTextureTriangles, true, TargetFaces, WhichMaterials);
return TextureSpecifiedFaces(
TargetUVLayer,
Collection,
GutterSize,
BakeAttributes,
AttributeSettings,
TextureOut,
ToTextureTriangles,
Progress);
}
bool TextureSpecifiedFaces(
int32 TargetUVLayer,
FGeometryCollection& Collection,
int32 GutterSize,
FIndex4i BakeAttributes,
const FTextureAttributeSettings& AttributeSettings,
TImageBuilder<FVector4f>& TextureOut,
TArrayView<bool> ToTextureTriangles,
FProgressCancel* Progress
)
{
int32 NumTextureTris = 0;
for (const bool& bSelected : ToTextureTriangles)
{
NumTextureTris += (bSelected) ? 1 : 0;
}
FGeomFlatUVMesh UVMesh(TargetUVLayer, &Collection, ToTextureTriangles, NumTextureTris);
FImageOccupancyMap OccupancyMap;
OccupancyMap.GutterSize = GutterSize;
OccupancyMap.Initialize(TextureOut.GetDimensions());
OccupancyMap.ComputeFromUVSpaceMesh(UVMesh, [](int32 TriangleID) { return TriangleID; });
FGeomMesh ToTextureMesh(TargetUVLayer, &Collection, ToTextureTriangles, NumTextureTris);
// Collect the external faces
TArray<bool> OutsideTriangles;
int32 NumOutsideTris = SetActiveTriangles(&Collection, OutsideTriangles, true, ETargetFaces::ExternalFaces, {});
FGeomMesh OutsideMesh(ToTextureMesh, OutsideTriangles, NumOutsideTris);
TMeshAABBTree3<FGeomMesh> OutsideSpatial(&OutsideMesh);
int DistanceToExternalIdx = BakeAttributes.IndexOf((int)EBakeAttributes::DistanceToExternal);
int AmbientIdx = BakeAttributes.IndexOf((int)EBakeAttributes::AmbientOcclusion);
int CurvatureIdx = BakeAttributes.IndexOf((int)EBakeAttributes::Curvature);
int NormalXIdx = BakeAttributes.IndexOf((int)EBakeAttributes::NormalX);
int NormalYIdx = BakeAttributes.IndexOf((int)EBakeAttributes::NormalY);
int NormalZIdx = BakeAttributes.IndexOf((int)EBakeAttributes::NormalZ);
FAxisAlignedBox3d Box = OutsideSpatial.GetBoundingBox();
int PosXIdx = Box.Width() > 0 ? BakeAttributes.IndexOf((int)EBakeAttributes::PositionX) : -1;
int PosYIdx = Box.Height() > 0 ? BakeAttributes.IndexOf((int)EBakeAttributes::PositionY) : -1;
int PosZIdx = Box.Depth() > 0 ? BakeAttributes.IndexOf((int)EBakeAttributes::PositionZ) : -1;
bool bNeedsDynamicMeshes = AmbientIdx > -1 || CurvatureIdx > -1;
TArray<int32> TransformIndices;
if (bNeedsDynamicMeshes)
{
for (int32 GeomIdx = 0; GeomIdx < Collection.TransformIndex.Num(); GeomIdx++)
{
int32 TransformIdx = Collection.TransformIndex[GeomIdx];
if (Collection.IsVisible(TransformIdx))
{
TransformIndices.Add(TransformIdx);
}
}
}
const float AmbientWorkPer = 1, CurvatureWorkPer = 10;
const int32 NumMeshes = TransformIndices.Num();
const float AmountOfWork =
float(AmbientIdx > -1) * AmbientWorkPer * NumMeshes +
float(CurvatureIdx > -1) * CurvatureWorkPer * NumMeshes;
const float WorkIncr = AmountOfWork > 0 ? 0.9f / AmountOfWork : 0.f;
FDynamicMeshCollection CollectionMeshes;
CollectionMeshes.bGenerateMeshToCollectionFaceMapping = true;
CollectionMeshes.Init(&Collection, TransformIndices, FTransform::Identity, false);
if (bNeedsDynamicMeshes)
{
// To bake only selected faces, unset texture on everything else
for (int MeshIdx = 0; MeshIdx < CollectionMeshes.Meshes.Num(); MeshIdx++)
{
FDynamicMeshCollection::FMeshData& MeshData = CollectionMeshes.Meshes[MeshIdx];
int32 TransformIdx = MeshData.TransformIndex;
FDynamicMesh3& Mesh = MeshData.AugMesh;
FMeshNormals::InitializeOverlayToPerVertexNormals(Mesh.Attributes()->PrimaryNormals(), true);
AugmentedDynamicMesh::InitializeOverlayToPerVertexUVs(Mesh, 1, TargetUVLayer); // build an overlay w/ *only* the uv layer we care about
FDynamicMeshUVOverlay* UV = Mesh.Attributes()->PrimaryUV();
for (int TID : Mesh.TriangleIndicesItr())
{
const int32 CollectionFaceIndex = MeshData.GetCollectionFaceFromMeshFace(TID);
if (ensure(CollectionFaceIndex != INDEX_NONE && ToTextureTriangles.IsValidIndex(CollectionFaceIndex)))
{
if (ToTextureTriangles[CollectionFaceIndex] == false)
{
UV->UnsetTriangle(TID);
}
}
}
}
}
if (Progress && Progress->Cancelled())
{
return false;
}
TArray64<float> CurvatureValues; // buffer to fill with curvature values
if (CurvatureIdx > -1)
{
int UseVoxRes = FMath::Max(8, AttributeSettings.Curvature_VoxelRes);
FImageDimensions Dimensions = TextureOut.GetDimensions();
int64 TexelsNum = Dimensions.Num();
CurvatureValues.SetNumZeroed(TexelsNum);
for (int MeshIdx = 0; MeshIdx < CollectionMeshes.Meshes.Num(); MeshIdx++)
{
if (Progress && Progress->Cancelled())
{
return false;
}
FProgressCancel::FProgressScope CurvatureScope(Progress, CurvatureWorkPer * WorkIncr);
int32 TransformIdx = CollectionMeshes.Meshes[MeshIdx].TransformIndex;
FDynamicMesh3& Mesh = CollectionMeshes.Meshes[MeshIdx].AugMesh;
FDynamicMeshAABBTree3 Spatial(&Mesh);
TFastWindingTree<FDynamicMesh3> FastWinding(&Spatial);
double ExtendBounds = Spatial.GetBoundingBox().MaxDim() * .01;
TImplicitSolidify<FDynamicMesh3> Solidify(&Mesh, &Spatial, &FastWinding);
Solidify.SetCellSizeAndExtendBounds(Spatial.GetBoundingBox(), ExtendBounds, UseVoxRes);
Solidify.WindingThreshold = AttributeSettings.Curvature_Winding;
Solidify.SurfaceSearchSteps = 3;
Solidify.bSolidAtBoundaries = true;
Solidify.ExtendBounds = ExtendBounds;
FDynamicMesh3 SolidMesh(&Solidify.Generate());
// Smooth mesh
double SmoothAlpha = AttributeSettings.Curvature_SmoothingPerStep;
TArray<FVector3d> PositionBuffer;
PositionBuffer.SetNumUninitialized(SolidMesh.MaxVertexID());
for (int VID : SolidMesh.VertexIndicesItr())
{
PositionBuffer[VID] = SolidMesh.GetVertex(VID);
}
UE::MeshDeformation::ComputeSmoothing_Forward(true, false, SolidMesh, [SmoothAlpha](int VID, bool bIsBoundary) { return SmoothAlpha; },
AttributeSettings.Curvature_SmoothingSteps, PositionBuffer);
for (int VID : SolidMesh.VertexIndicesItr())
{
SolidMesh.SetVertex(VID, PositionBuffer[VID]);
}
FDynamicMeshAABBTree3 SolidSpatial(&SolidMesh);
FMeshImageBakingCache BakeCache;
BakeCache.SetGutterSize(GutterSize);
BakeCache.SetDetailMesh(&SolidMesh, &SolidSpatial);
BakeCache.SetBakeTargetMesh(&Mesh);
BakeCache.SetDimensions(Dimensions);
BakeCache.SetUVLayer(0);
// set distance to search for correspondences based on the voxel size
double Thickness = AttributeSettings.Curvature_ThicknessFactor * Spatial.GetBoundingBox().MaxDim() / (float)UseVoxRes;
BakeCache.SetThickness(Thickness);
BakeCache.ValidateCache();
FMeshCurvatureMapBaker CurvatureBaker;
CurvatureBaker.bOverrideCurvatureRange = true;
CurvatureBaker.OverrideRangeMax = AttributeSettings.Curvature_MaxValue;
CurvatureBaker.UseColorMode = FMeshCurvatureMapBaker::EColorMode::BlackGrayWhite;
CurvatureBaker.SetCache(&BakeCache);
CurvatureBaker.BlurRadius = AttributeSettings.bCurvature_Blur ? AttributeSettings.Curvature_BlurRadius : 0.0;
CurvatureBaker.Bake();
const TUniquePtr<TImageBuilder<FVector3f>>& Result = CurvatureBaker.GetResult();
// copy scalar curvature values out to the accumulated curvature buffer
for (int64 LinearIdx = 0; LinearIdx < TexelsNum; LinearIdx++)
{
float& CurvatureOut = CurvatureValues[LinearIdx];
CurvatureOut = FMathf::Max(CurvatureOut, Result->GetPixel(LinearIdx).X);
}
}
}
TArray64<float> AmbientValues; // buffer to fill with ambient occlusion values
if (AmbientIdx > -1)
{
FImageDimensions Dimensions = TextureOut.GetDimensions();
int64 TexelsNum = Dimensions.Num();
AmbientValues.Init(1.0f, TexelsNum);
for (int MeshIdx = 0; MeshIdx < CollectionMeshes.Meshes.Num(); MeshIdx++)
{
if (Progress && Progress->Cancelled())
{
return false;
}
FProgressCancel::FProgressScope CurvatureScope(Progress, AmbientWorkPer * WorkIncr);
int32 TransformIdx = CollectionMeshes.Meshes[MeshIdx].TransformIndex;
FDynamicMesh3& Mesh = CollectionMeshes.Meshes[MeshIdx].AugMesh;
FDynamicMeshAABBTree3 Spatial(&Mesh);
FMeshImageBakingCache BakeCache;
BakeCache.SetGutterSize(GutterSize);
BakeCache.SetDetailMesh(&Mesh, &Spatial);
BakeCache.SetBakeTargetMesh(&Mesh);
BakeCache.SetDimensions(Dimensions);
BakeCache.SetUVLayer(0);
BakeCache.SetCorrespondenceStrategy(FMeshImageBakingCache::ECorrespondenceStrategy::Identity);
// thickness shouldn't matter because we're using Identity ECorrespondenceStrategy::Identity
BakeCache.SetThickness(KINDA_SMALL_NUMBER);
BakeCache.ValidateCache();
FMeshOcclusionMapBaker OcclusionBaker;
OcclusionBaker.SetCache(&BakeCache);
OcclusionBaker.OcclusionType = EOcclusionMapType::AmbientOcclusion;
OcclusionBaker.NumOcclusionRays = AttributeSettings.AO_Rays;
OcclusionBaker.MaxDistance = AttributeSettings.AO_MaxDistance == 0 ? TNumericLimits<double>::Max() : AttributeSettings.AO_MaxDistance;
OcclusionBaker.BiasAngleDeg = AttributeSettings.AO_BiasAngleDeg;
OcclusionBaker.BlurRadius = AttributeSettings.bAO_Blur ? AttributeSettings.AO_BlurRadius : 0.0;
OcclusionBaker.Bake();
const TUniquePtr<TImageBuilder<FVector3f>>& Result = OcclusionBaker.GetResult(FMeshOcclusionMapBaker::EResult::AmbientOcclusion);
// copy scalar ambient values out to the accumulated ambient buffer
for (int64 LinearIdx = 0; LinearIdx < TexelsNum; LinearIdx++)
{
float& AmbientOut = AmbientValues[LinearIdx];
AmbientOut = FMathf::Min(AmbientOut, Result->GetPixel(LinearIdx).X);
}
}
}
if (Progress && Progress->Cancelled())
{
return false;
}
FProgressCancel::FProgressScope EndBake = FProgressCancel::CreateScopeTo(Progress, 1);
ParallelFor(OccupancyMap.Dimensions.GetHeight(),
[&AttributeSettings, &TextureOut, &UVMesh, &OccupancyMap, &ToTextureMesh, &OutsideSpatial,
&DistanceToExternalIdx, &AmbientIdx, &NormalXIdx, &NormalYIdx, &NormalZIdx, &PosXIdx, &PosYIdx, &PosZIdx](int32 Y)
{
for (int32 X = 0; X < OccupancyMap.Dimensions.GetWidth(); X++)
{
int64 LinearCoord = OccupancyMap.Dimensions.GetIndex(X, Y);
if (OccupancyMap.IsInterior(LinearCoord))
{
FVector4f OutColor = FVector4f::One();
int32 TID = OccupancyMap.TexelQueryTriangle[LinearCoord];
FVector2f UV = OccupancyMap.TexelQueryUV[LinearCoord];
// Note this is the slowest way to get barycentric coordinates out of a point and a 2D triangle,
// ... but it's the one we currently have that's robust to (and gives good answers on) degenerate triangles
FDistPoint3Triangle3d DistQuery = TMeshQueries<FGeomFlatUVMesh>::TriangleDistance(UVMesh, TID, FVector3d(UV.X, UV.Y, 0));
FVector3d Bary = DistQuery.TriangleBaryCoords;
FVector3f Normal(0, 0, 1);
bool bNeedsNormal = NormalXIdx > -1 || NormalYIdx > -1 || NormalZIdx > -1 || AmbientIdx > -1;
if (bNeedsNormal)
{
Normal = ToTextureMesh.GetInterpolatedNormal(TID, Bary);
}
if (DistanceToExternalIdx > -1 || PosXIdx > -1 || PosYIdx > -1 || PosZIdx > -1)
{
FTriangle3d Tri;
ToTextureMesh.GetTriVertices(TID, Tri.V[0], Tri.V[1], Tri.V[2]);
FVector3d InsidePoint = Tri.BarycentricPoint(Bary);
if (DistanceToExternalIdx > -1)
{
double DistanceSq;
OutsideSpatial.FindNearestTriangle(InsidePoint, DistanceSq, AttributeSettings.ToExternal_MaxDistance);
float PercentDistance = FMathf::Min(1.0f, static_cast<float>(FMathd::Sqrt(DistanceSq) / AttributeSettings.ToExternal_MaxDistance));
checkSlow(FMath::IsFinite(PercentDistance));
OutColor[DistanceToExternalIdx] = PercentDistance;
}
auto SetSpatial = [&OutsideSpatial, &InsidePoint, &OutColor](int TargetIdx, int Dim)
{
if (TargetIdx > -1)
{
double Min = OutsideSpatial.GetBoundingBox().Min[Dim], Max = OutsideSpatial.GetBoundingBox().Max[Dim];
checkSlow(Min != Max);
float PercentAlong = static_cast<float>( (InsidePoint[Dim] - Min) / (Max - Min) );
OutColor[TargetIdx] = PercentAlong;
}
};
SetSpatial(PosXIdx, 0);
SetSpatial(PosYIdx, 1);
SetSpatial(PosZIdx, 2);
}
auto SetNormal = [&Normal, &OutColor](int TargetIdx, int Dim)
{
if (TargetIdx > -1)
{
float NormalVal = Normal[Dim];
{
NormalVal = (1 + NormalVal) * .5f; // compress from [-1,1] to [0,1] range
}
OutColor[TargetIdx] = NormalVal;
}
};
SetNormal(NormalXIdx, 0);
SetNormal(NormalYIdx, 1);
SetNormal(NormalZIdx, 2);
TextureOut.SetPixel(LinearCoord, OutColor);
}
}
});
if (Progress && Progress->Cancelled())
{
return false;
}
auto CopyValuesToChannel = [&TextureOut, &OccupancyMap](int ChannelIdx, TArray64<float>& Values)
{
if (ChannelIdx > -1)
{
int64 TexelsNum = OccupancyMap.Dimensions.Num();
for (int64 LinearIdx = 0; LinearIdx < TexelsNum; LinearIdx++)
{
FVector4f Pixel = TextureOut.GetPixel(LinearIdx);
Pixel[ChannelIdx] = Values[LinearIdx];
TextureOut.SetPixel(LinearIdx, Pixel);
}
}
};
CopyValuesToChannel(AmbientIdx, AmbientValues);
CopyValuesToChannel(CurvatureIdx, CurvatureValues);
if (AttributeSettings.ClearGutterChannel > -1 && AttributeSettings.ClearGutterChannel < 4)
{
for (TTuple<int64, int64>& GutterToInside : OccupancyMap.GutterTexels)
{
FVector4f Pixel = TextureOut.GetPixel(GutterToInside.Value);
Pixel[AttributeSettings.ClearGutterChannel] = 0.0f;
TextureOut.SetPixel(GutterToInside.Key, Pixel);
}
}
else
{
for (TTuple<int64, int64>& GutterToInside : OccupancyMap.GutterTexels)
{
TextureOut.CopyPixel(GutterToInside.Value, GutterToInside.Key);
}
}
return true;
}
}} // namespace UE::PlanarCut