Files
UnrealEngineUWP/Engine/Source/Developer/MeshDescriptionOperations/Private/LayoutUV.cpp
Jack Porter 079be7f538 Merging //UE4/Dev-Main to Dev-Mobile (//UE4/Dev-Mobile)
#rb None
#jira 0

[CL 4293080 by Jack Porter in Dev-Mobile branch]
2018-08-16 13:53:43 -04:00

1122 lines
31 KiB
C++

// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#include "LayoutUV.h"
#include "DisjointSet.h"
#include "MeshDescriptionOperations.h"
#include "Algo/IntroSort.h"
DEFINE_LOG_CATEGORY_STATIC(LogMeshDescriptionLayoutUV, Warning, All);
#define CHART_JOINING 1
namespace MeshDescriptionOp
{
FLayoutUV::FLayoutUV(FMeshDescription& InMesh, uint32 InSrcChannel, uint32 InDstChannel, uint32 InTextureResolution)
: MeshDescription(InMesh)
, SrcChannel(InSrcChannel)
, DstChannel(InDstChannel)
, TextureResolution(InTextureResolution)
, TotalUVArea(0.0f)
, LayoutRaster(TextureResolution, TextureResolution)
, ChartRaster(TextureResolution, TextureResolution)
, BestChartRaster(TextureResolution, TextureResolution)
, ChartShader(&ChartRaster)
, LayoutVersion(FMeshDescriptionOperations::ELightmapUVVersion::Latest)
, NextMeshChartId( 0 )
{}
void FLayoutUV::FindCharts(const TMultiMap<int32, int32>& OverlappingCorners)
{
const float ThreshUVsAreSame = GetUVEqualityThreshold();
double Begin = FPlatformTime::Seconds();
uint32 NumTris = 0;
for (const FPolygonID PolygonID : MeshDescription.Polygons().GetElementIDs())
{
NumTris += MeshDescription.GetPolygonTriangles(PolygonID).Num();
}
uint32 NumIndexes = NumTris * 3;
TArray< int32 > TranslatedMatches;
TranslatedMatches.SetNumUninitialized(NumIndexes);
TexCoords.SetNumUninitialized(NumIndexes);
int32 WedgeIndex = 0;
VertexIndexToID.SetNumUninitialized(NumIndexes);
VertexIDToIndex.SetNumUninitialized(MeshDescription.VertexInstances().GetArraySize());
const TVertexInstanceAttributeArray<FVector2D>& VertexUVs = MeshDescription.VertexInstanceAttributes().GetAttributes<FVector2D>(MeshAttribute::VertexInstance::TextureCoordinate, SrcChannel);
for (const FPolygonID PolygonID : MeshDescription.Polygons().GetElementIDs())
{
const TArray<FMeshTriangle>& Triangles = MeshDescription.GetPolygonTriangles(PolygonID);
for (const FMeshTriangle MeshTriangle : Triangles)
{
for (int32 Corner = 0; Corner < 3; ++Corner)
{
const FVertexInstanceID VertexInstanceID = MeshTriangle.GetVertexInstanceID(Corner);
TranslatedMatches[WedgeIndex] = -1;
TexCoords[WedgeIndex] = VertexUVs[VertexInstanceID];
VertexIndexToID[WedgeIndex] = VertexInstanceID.GetValue();
VertexIDToIndex[VertexInstanceID.GetValue()] = WedgeIndex;
++WedgeIndex;
}
}
}
// Build disjoint set
FDisjointSet DisjointSet(NumTris);
for (uint32 i = 0; i < NumIndexes; i++)
{
for (auto It = OverlappingCorners.CreateConstKeyIterator(i); It; ++It)
{
// OverlappingCorners has been computed with ids of vertex instances.
uint32 j = VertexIDToIndex[It.Value()];
if (j > i)
{
const uint32 TriI = i / 3;
const uint32 TriJ = j / 3;
bool bUnion = false;
#if CHART_JOINING
bool bPositionMatch = PositionsMatch(i, j);
if (bPositionMatch)
{
uint32 i1 = 3 * TriI + (i + 1) % 3;
uint32 i2 = 3 * TriI + (i + 2) % 3;
uint32 j1 = 3 * TriJ + (j + 1) % 3;
uint32 j2 = 3 * TriJ + (j + 2) % 3;
bool bEdgeMatch21 = PositionsMatch(i2, j1);
bool bEdgeMatch12 = PositionsMatch(i1, j2);
if (bEdgeMatch21 || bEdgeMatch12)
{
uint32 ie = bEdgeMatch21 ? i2 : i1;
uint32 je = bEdgeMatch21 ? j1 : j2;
bool bUVMatch = UVsMatch(i, j) && UVsMatch(ie, je);
bool bUVWindingMatch = TriangleUVArea(TriI) * TriangleUVArea(TriJ) >= 0.0f;
if (bUVMatch && bUVWindingMatch)
{
bUnion = true;
}
else if (NormalsMatch(i, j) && NormalsMatch(ie, je))
{
// Chart edge
FVector2D EdgeUVi = TexCoords[ie] - TexCoords[i];
FVector2D EdgeUVj = TexCoords[je] - TexCoords[j];
// Would these edges match if the charts were translated
bool bTranslatedUVMatch = (EdgeUVi - EdgeUVj).IsNearlyZero(ThreshUVsAreSame);
if (bTranslatedUVMatch)
{
// Note: may be mirrored
// TODO should these be restricted to axis aligned edges?
uint32 EdgeI = bEdgeMatch21 ? i2 : i;
uint32 EdgeJ = bEdgeMatch21 ? j : j2;
// Only allow one match per edge
if (TranslatedMatches[EdgeI] < 0 &&
TranslatedMatches[EdgeJ] < 0)
{
TranslatedMatches[EdgeI] = EdgeJ;
TranslatedMatches[EdgeJ] = EdgeI;
}
}
}
}
}
#else
if (VertsMatch(i, j))
{
// Edge must match as well (same winding)
if (VertsMatch(3 * TriI + (i - 1) % 3, 3 * TriJ + (j + 1) % 3) ||
VertsMatch(3 * TriI + (i + 1) % 3, 3 * TriJ + (j - 1) % 3))
{
// Check for UV winding match too
if (TriangleUVArea(TriI) * TriangleUVArea(TriJ) >= 0.0f)
{
bUnion = true;
}
}
}
#endif
if (bUnion)
{
// TODO solve spiral case by checking sets for UV overlap
DisjointSet.Union(TriI, TriJ);
}
}
}
}
// Sort tris by chart
SortedTris.SetNumUninitialized(NumTris);
for (uint32 i = 0; i < NumTris; i++)
{
// Flatten disjoint set path
DisjointSet.Find(i);
SortedTris[i] = i;
}
struct FCompareTris
{
FDisjointSet* DisjointSet;
FCompareTris(FDisjointSet* InDisjointSet)
: DisjointSet(InDisjointSet)
{}
FORCEINLINE bool operator()(uint32 A, uint32 B) const
{
return (*DisjointSet)[A] < (*DisjointSet)[B];
}
};
Algo::IntroSort(SortedTris, FCompareTris(&DisjointSet));
TMap< uint32, int32 > DisjointSetToChartMap;
const TVertexAttributeArray<FVector>& VertexPositions = MeshDescription.VertexAttributes().GetAttributes<FVector>(MeshAttribute::Vertex::Position);
// Build Charts
for (uint32 Tri = 0; Tri < NumTris; )
{
int32 i = Charts.AddUninitialized();
FMeshChart& Chart = Charts[i];
Chart.Id = NextMeshChartId++;
Chart.MinUV = FVector2D(FLT_MAX, FLT_MAX);
Chart.MaxUV = FVector2D(-FLT_MAX, -FLT_MAX);
Chart.UVArea = 0.0f;
Chart.WorldScale = FVector2D::ZeroVector;
FMemory::Memset(Chart.Join, 0xff);
Chart.FirstTri = Tri;
uint32 ChartID = DisjointSet[SortedTris[Tri]];
DisjointSetToChartMap.Add(ChartID, i);
for (; Tri < NumTris && DisjointSet[SortedTris[Tri]] == ChartID; Tri++)
{
// Calculate chart bounds
FVector Positions[3];
FVector2D UVs[3];
for (int k = 0; k < 3; k++)
{
uint32 Index = 3 * SortedTris[Tri] + k;
FVertexInstanceID VertexInstanceID(VertexIndexToID[Index]);
Positions[k] = VertexPositions[MeshDescription.GetVertexInstanceVertex(VertexInstanceID)];
UVs[k] = TexCoords[Index];
Chart.MinUV.X = FMath::Min(Chart.MinUV.X, UVs[k].X);
Chart.MinUV.Y = FMath::Min(Chart.MinUV.Y, UVs[k].Y);
Chart.MaxUV.X = FMath::Max(Chart.MaxUV.X, UVs[k].X);
Chart.MaxUV.Y = FMath::Max(Chart.MaxUV.Y, UVs[k].Y);
}
FVector Edge1 = Positions[1] - Positions[0];
FVector Edge2 = Positions[2] - Positions[0];
float Area = 0.5f * (Edge1 ^ Edge2).Size();
FVector2D EdgeUV1 = UVs[1] - UVs[0];
FVector2D EdgeUV2 = UVs[2] - UVs[0];
float UVArea = 0.5f * FMath::Abs(EdgeUV1.X * EdgeUV2.Y - EdgeUV1.Y * EdgeUV2.X);
FVector2D UVLength;
UVLength.X = (EdgeUV2.Y * Edge1 - EdgeUV1.Y * Edge2).Size();
UVLength.Y = (-EdgeUV2.X * Edge1 + EdgeUV1.X * Edge2).Size();
Chart.WorldScale += UVLength;
Chart.UVArea += UVArea;
}
Chart.LastTri = Tri;
#if !CHART_JOINING
if (LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::SmallChartPacking)
{
Chart.WorldScale /= FMath::Max(Chart.UVArea, 1e-8f);
}
else
{
if (Chart.UVArea > 1e-4f)
{
Chart.WorldScale /= Chart.UVArea;
}
else
{
Chart.WorldScale = FVector2D::ZeroVector;
}
}
TotalUVArea += Chart.UVArea * Chart.WorldScale.X * Chart.WorldScale.Y;
#endif
}
#if CHART_JOINING
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& Chart = Charts[i];
for (uint32 Tri = Chart.FirstTri; Tri < Chart.LastTri; Tri++)
{
for (int k = 0; k < 3; k++)
{
uint32 Index = 3 * SortedTris[Tri] + k;
if (TranslatedMatches[Index] >= 0)
{
checkSlow(TranslatedMatches[TranslatedMatches[Index]] == Index);
uint32 V0i = Index;
uint32 V0j = TranslatedMatches[Index];
uint32 TriI = V0i / 3;
uint32 TriJ = V0j / 3;
if (TriJ <= TriI)
{
// Only need to consider one direction
continue;
}
uint32 V1i = 3 * TriI + (V0i + 1) % 3;
uint32 V1j = 3 * TriJ + (V0j + 1) % 3;
int32 ChartI = i;
int32 ChartJ = DisjointSetToChartMap[DisjointSet[TriJ]];
FVector2D UV0i = TexCoords[V0i];
FVector2D UV1i = TexCoords[V1i];
FVector2D UV0j = TexCoords[V0j];
FVector2D UV1j = TexCoords[V1j];
FVector2D EdgeUVi = UV1i - UV0i;
FVector2D EdgeUVj = UV1j - UV0j;
bool bMirrored = TriangleUVArea(TriI) * TriangleUVArea(TriJ) < 0.0f;
FVector2D EdgeOffset0 = UV0i - UV1j;
FVector2D EdgeOffset1 = UV1i - UV0j;
checkSlow((EdgeOffset0 - EdgeOffset1).IsNearlyZero(ThreshUVsAreSame));
FVector2D Translation = EdgeOffset0;
FMeshChart& ChartA = Charts[ChartI];
FMeshChart& ChartB = Charts[ChartJ];
for (uint32 Side = 0; Side < 4; Side++)
{
// Join[] = { left, right, bottom, top }
// FIXME
if (bMirrored)
continue;
if (ChartA.Join[Side ^ 0] != -1 ||
ChartB.Join[Side ^ 1] != -1)
{
// Already joined with something else
continue;
}
uint32 Sign = Side & 1;
uint32 Axis = Side >> 1;
bool bAxisAligned = FMath::Abs(EdgeUVi[Axis]) < ThreshUVsAreSame;
bool bBorderA = FMath::Abs(UV0i[Axis] - (Sign ^ 0 ? Chart.MaxUV[Axis] : Chart.MinUV[Axis])) < ThreshUVsAreSame;
bool bBorderB = FMath::Abs(UV0j[Axis] - (Sign ^ 1 ? Chart.MaxUV[Axis] : Chart.MinUV[Axis])) < ThreshUVsAreSame;
// FIXME mirrored
if (!bAxisAligned || !bBorderA || !bBorderB)
{
// Edges weren't on matching rectangle borders
continue;
}
FVector2D CenterA = 0.5f * (ChartA.MinUV + ChartA.MaxUV);
FVector2D CenterB = 0.5f * (ChartB.MinUV + ChartB.MaxUV);
FVector2D ExtentA = 0.5f * (ChartA.MaxUV - ChartA.MinUV);
FVector2D ExtentB = 0.5f * (ChartB.MaxUV - ChartB.MinUV);
// FIXME mirrored
CenterB += Translation;
FVector2D CenterDiff = CenterA - CenterB;
FVector2D ExtentDiff = ExtentA - ExtentB;
FVector2D Separation = ExtentA + ExtentB + CenterDiff * (Sign ? 1.0f : -1.0f);
bool bCenterMatch = FMath::Abs(CenterDiff[Axis ^ 1]) < ThreshUVsAreSame;
bool bExtentMatch = FMath::Abs(ExtentDiff[Axis ^ 1]) < ThreshUVsAreSame;
bool bSeparate = FMath::Abs(Separation[Axis ^ 0]) < ThreshUVsAreSame;
if (!bCenterMatch || !bExtentMatch || !bSeparate)
{
// Rectangles don't match up after translation
continue;
}
// Found a valid edge join
ChartA.Join[Side ^ 0] = ChartJ;
ChartB.Join[Side ^ 1] = ChartI;
break;
}
}
}
}
}
TArray< uint32 > JoinedSortedTris;
JoinedSortedTris.Reserve(NumTris);
// Detect loops
for (uint32 Axis = 0; Axis < 2; Axis++)
{
uint32 Side = Axis << 1;
for (int32 i = 0; i < Charts.Num(); i++)
{
int32 j = Charts[i].Join[Side ^ 1];
while (j != -1)
{
int32 Next = Charts[j].Join[Side ^ 1];
if (Next == i)
{
// Break loop
Charts[i].Join[Side ^ 0] = -1;
Charts[j].Join[Side ^ 1] = -1;
break;
}
j = Next;
}
}
}
// Join rows first, then columns
for (uint32 Axis = 0; Axis < 2; Axis++)
{
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& ChartA = Charts[i];
if (ChartA.FirstTri == ChartA.LastTri)
{
// Empty chart
continue;
}
for (uint32 Side = 0; Side < 4; Side++)
{
if (ChartA.Join[Side] != -1)
{
FMeshChart& ChartB = Charts[ChartA.Join[Side]];
check(ChartB.Join[Side ^ 1] == i);
check(ChartB.FirstTri != ChartB.LastTri);
}
}
}
NumTris = 0;
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& Chart = Charts[i];
NumTris += Chart.LastTri - Chart.FirstTri;
}
check(NumTris == SortedTris.Num());
NumTris = 0;
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& ChartA = Charts[i];
if (ChartA.FirstTri == ChartA.LastTri)
{
// Empty chart
continue;
}
uint32 Side = Axis << 1;
// Find start (left, bottom)
if (ChartA.Join[Side ^ 0] == -1)
{
// Add original tris
NumTris += ChartA.LastTri - ChartA.FirstTri;
// Continue joining until no more to the (right, top)
int32 Next = ChartA.Join[Side ^ 1];
while (Next != -1)
{
FMeshChart& ChartB = Charts[Next];
NumTris += ChartB.LastTri - ChartB.FirstTri;
Next = ChartB.Join[Side ^ 1];
}
}
}
check(NumTris == SortedTris.Num());
#if 1
NumTris = 0;
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& ChartA = Charts[i];
if (ChartA.FirstTri == ChartA.LastTri)
{
// Empty chart
continue;
}
// Join[] = { left, right, bottom, top }
uint32 Side = Axis << 1;
// Find start (left, bottom)
if (ChartA.Join[Side ^ 0] == -1)
{
uint32 FirstTri = JoinedSortedTris.Num();
// Add original tris
for (uint32 Tri = ChartA.FirstTri; Tri < ChartA.LastTri; Tri++)
{
JoinedSortedTris.Add(SortedTris[Tri]);
}
NumTris += ChartA.LastTri - ChartA.FirstTri;
// Continue joining until no more to the (right, top)
while (ChartA.Join[Side ^ 1] != -1)
{
FMeshChart& ChartB = Charts[ChartA.Join[Side ^ 1]];
check(ChartB.FirstTri != ChartB.LastTri);
FVector2D Translation = ChartA.MinUV - ChartB.MinUV;
Translation[Axis] += ChartA.MaxUV[Axis] - ChartA.MinUV[Axis];
for (uint32 Tri = ChartB.FirstTri; Tri < ChartB.LastTri; Tri++)
{
JoinedSortedTris.Add(SortedTris[Tri]);
for (int k = 0; k < 3; k++)
{
TexCoords[3 * SortedTris[Tri] + k] += Translation;
}
}
NumTris += ChartB.LastTri - ChartB.FirstTri;
ChartA.Join[Side ^ 1] = ChartB.Join[Side ^ 1];
ChartA.MaxUV[Axis] += ChartB.MaxUV[Axis] - ChartB.MinUV[Axis];
if( LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::ChartJoiningLFix )
{
// Fixing joined chart MaxUV value to properly inflate non-joined axis extent
ChartA.MaxUV[ Axis ^ 1 ] = FMath::Max( ChartA.MaxUV[ Axis ^ 1 ], ChartA.MinUV[ Axis ^ 1 ] + ( ChartB.MaxUV[ Axis ^ 1 ] - ChartB.MinUV[ Axis ^ 1 ] ) );
}
ChartA.WorldScale += ChartB.WorldScale;
ChartA.UVArea += ChartB.UVArea;
ChartB.FirstTri = 0;
ChartB.LastTri = 0;
ChartB.UVArea = 0.0f;
DisconnectChart(ChartB, Side ^ 2);
DisconnectChart(ChartB, Side ^ 3);
}
ChartA.FirstTri = FirstTri;
ChartA.LastTri = JoinedSortedTris.Num();
}
else
{
// Make sure a starting chart could connect to this
FMeshChart& ChartB = Charts[ChartA.Join[Side ^ 0]];
check(ChartB.Join[Side ^ 1] == i);
check(ChartB.FirstTri != ChartB.LastTri);
}
}
check(NumTris == SortedTris.Num());
check(SortedTris.Num() == JoinedSortedTris.Num());
Exchange(SortedTris, JoinedSortedTris);
JoinedSortedTris.Reset();
#endif
}
// Clean out empty charts
for (int32 i = 0; i < Charts.Num(); i++)
{
while (i < Charts.Num() && Charts[i].FirstTri == Charts[i].LastTri)
{
Charts.RemoveAtSwap(i);
}
}
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& Chart = Charts[i];
if (LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::SmallChartPacking)
{
Chart.WorldScale /= FMath::Max(Chart.UVArea, 1e-8f);
}
else
{
if (Chart.UVArea > 1e-4f)
{
Chart.WorldScale /= Chart.UVArea;
}
else
{
Chart.WorldScale = FVector2D::ZeroVector;
}
}
TotalUVArea += Chart.UVArea * Chart.WorldScale.X * Chart.WorldScale.Y;
}
#endif
double End = FPlatformTime::Seconds();
UE_LOG(LogMeshDescriptionLayoutUV, Display, TEXT("FindCharts: %s"), *FPlatformTime::PrettyTime(End - Begin));
}
bool FLayoutUV::FindBestPacking()
{
if ((uint32)Charts.Num() > TextureResolution * TextureResolution || TotalUVArea == 0.f)
{
// More charts than texels
return false;
}
const float LinearSearchStart = 0.5f;
const float LinearSearchStep = 0.5f;
const int32 BinarySearchSteps = 6;
float UVScaleFail = TextureResolution * FMath::Sqrt(1.0f / TotalUVArea);
float UVScalePass = TextureResolution * FMath::Sqrt(LinearSearchStart / TotalUVArea);
// Linear search for first fit
while (1)
{
ScaleCharts(UVScalePass);
bool bFit = PackCharts();
if (bFit)
{
break;
}
UVScaleFail = UVScalePass;
UVScalePass *= LinearSearchStep;
}
// Binary search for best fit
for (int32 i = 0; i < BinarySearchSteps; i++)
{
float UVScale = 0.5f * (UVScaleFail + UVScalePass);
ScaleCharts(UVScale);
bool bFit = PackCharts();
if (bFit)
{
UVScalePass = UVScale;
}
else
{
UVScaleFail = UVScale;
}
}
// TODO store packing scale/bias separate so this isn't necessary
ScaleCharts(UVScalePass);
PackCharts();
return true;
}
void FLayoutUV::ScaleCharts(float UVScale)
{
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& Chart = Charts[i];
Chart.UVScale = Chart.WorldScale * UVScale;
}
if ( LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::ScaleChartsOrderingFix )
{
// Unsort the charts to make sure ScaleCharts always return the same ordering
Algo::IntroSort( Charts, []( const FMeshChart& A, const FMeshChart& B )
{
return A.Id < B.Id;
});
}
// Scale charts such that they all fit and roughly total the same area as before
#if 1
float UniformScale = 1.0f;
for (int i = 0; i < 1000; i++)
{
uint32 NumMaxedOut = 0;
float ScaledUVArea = 0.0f;
for (int32 ChartIndex = 0; ChartIndex < Charts.Num(); ChartIndex++)
{
FMeshChart& Chart = Charts[ChartIndex];
FVector2D ChartSize = Chart.MaxUV - Chart.MinUV;
FVector2D ChartSizeScaled = ChartSize * Chart.UVScale * UniformScale;
const float MaxChartEdge = TextureResolution - 1.0f;
const float LongestChartEdge = FMath::Max(ChartSizeScaled.X, ChartSizeScaled.Y);
const float Epsilon = 0.01f;
if (LongestChartEdge + Epsilon > MaxChartEdge)
{
// Rescale oversized charts to fit
Chart.UVScale.X = MaxChartEdge / FMath::Max(ChartSize.X, ChartSize.Y);
Chart.UVScale.Y = MaxChartEdge / FMath::Max(ChartSize.X, ChartSize.Y);
NumMaxedOut++;
}
else
{
Chart.UVScale.X *= UniformScale;
Chart.UVScale.Y *= UniformScale;
}
ScaledUVArea += Chart.UVArea * Chart.UVScale.X * Chart.UVScale.Y;
}
if (NumMaxedOut == 0)
{
// No charts maxed out so no need to rebalance
break;
}
if (NumMaxedOut == Charts.Num())
{
// All charts are maxed out
break;
}
// Scale up smaller charts to maintain expected total area
// Want ScaledUVArea == TotalUVArea * UVScale^2
float RebalanceScale = UVScale * FMath::Sqrt(TotalUVArea / ScaledUVArea);
if (RebalanceScale < 1.01f)
{
// Stop if further rebalancing is minor
break;
}
UniformScale = RebalanceScale;
}
#endif
#if 1
float NonuniformScale = 1.0f;
for (int i = 0; i < 1000; i++)
{
uint32 NumMaxedOut = 0;
float ScaledUVArea = 0.0f;
for (int32 ChartIndex = 0; ChartIndex < Charts.Num(); ChartIndex++)
{
FMeshChart& Chart = Charts[ChartIndex];
for (int k = 0; k < 2; k++)
{
const float MaximumChartSize = TextureResolution - 1.0f;
const float ChartSize = Chart.MaxUV[k] - Chart.MinUV[k];
const float ChartSizeScaled = ChartSize * Chart.UVScale[k] * NonuniformScale;
const float Epsilon = 0.01f;
if (ChartSizeScaled + Epsilon > MaximumChartSize)
{
// Scale oversized charts to max size
Chart.UVScale[k] = MaximumChartSize / ChartSize;
NumMaxedOut++;
}
else
{
Chart.UVScale[k] *= NonuniformScale;
}
}
ScaledUVArea += Chart.UVArea * Chart.UVScale.X * Chart.UVScale.Y;
}
if (NumMaxedOut == 0)
{
// No charts maxed out so no need to rebalance
break;
}
if (NumMaxedOut == Charts.Num() * 2)
{
// All charts are maxed out in both dimensions
break;
}
// Scale up smaller charts to maintain expected total area
// Want ScaledUVArea == TotalUVArea * UVScale^2
float RebalanceScale = UVScale * FMath::Sqrt(TotalUVArea / ScaledUVArea);
if (RebalanceScale < 1.01f)
{
// Stop if further rebalancing is minor
break;
}
NonuniformScale = RebalanceScale;
}
#endif
// Sort charts from largest to smallest
struct FCompareCharts
{
FORCEINLINE bool operator()(const FMeshChart& A, const FMeshChart& B) const
{
// Rect area
FVector2D ChartRectA = (A.MaxUV - A.MinUV) * A.UVScale;
FVector2D ChartRectB = (B.MaxUV - B.MinUV) * B.UVScale;
return ChartRectA.X * ChartRectA.Y > ChartRectB.X * ChartRectB.Y;
}
};
Algo::IntroSort(Charts, FCompareCharts());
}
bool FLayoutUV::PackCharts()
{
uint32 RasterizeCycles = 0;
uint32 FindCycles = 0;
double BeginPackCharts = FPlatformTime::Seconds();
LayoutRaster.Clear();
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& Chart = Charts[i];
// Try different orientations and pick best
int32 BestOrientation = -1;
FAllocator2D::FRect BestRect = { ~0u, ~0u, ~0u, ~0u };
for (int32 Orientation = 0; Orientation < 8; Orientation++)
{
// TODO If any dimension is less than 1 pixel shrink dimension to zero
OrientChart(Chart, Orientation);
FVector2D ChartSize = Chart.MaxUV - Chart.MinUV;
ChartSize = ChartSize.X * Chart.PackingScaleU + ChartSize.Y * Chart.PackingScaleV;
// Only need half pixel dilate for rects
FAllocator2D::FRect Rect;
Rect.X = 0;
Rect.Y = 0;
Rect.W = FMath::CeilToInt(FMath::Abs(ChartSize.X) + 1.0f);
Rect.H = FMath::CeilToInt(FMath::Abs(ChartSize.Y) + 1.0f);
// Just in case lack of precision pushes it over
Rect.W = FMath::Min(TextureResolution, Rect.W);
Rect.H = FMath::Min(TextureResolution, Rect.H);
const bool bRectPack = false;
if (bRectPack)
{
if (LayoutRaster.Find(Rect))
{
// Is best?
if (Rect.X + Rect.Y * TextureResolution < BestRect.X + BestRect.Y * TextureResolution)
{
BestOrientation = Orientation;
BestRect = Rect;
}
}
else
{
continue;
}
}
else
{
if (LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::Segments && Orientation % 4 == 1)
{
ChartRaster.FlipX(Rect, LayoutVersion);
}
else if (LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::Segments && Orientation % 4 == 3)
{
ChartRaster.FlipY(Rect);
}
else
{
int32 BeginRasterize = FPlatformTime::Cycles();
RasterizeChart(Chart, Rect.W, Rect.H);
RasterizeCycles += FPlatformTime::Cycles() - BeginRasterize;
}
bool bFound = false;
uint32 BeginFind = FPlatformTime::Cycles();
if (LayoutVersion == FMeshDescriptionOperations::ELightmapUVVersion::BitByBit)
{
bFound = LayoutRaster.FindBitByBit(Rect, ChartRaster);
}
else if (LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::Segments)
{
bFound = LayoutRaster.FindWithSegments(Rect, BestRect, ChartRaster);
}
FindCycles += FPlatformTime::Cycles() - BeginFind;
if (bFound)
{
// Is best?
if (Rect.X + Rect.Y * TextureResolution < BestRect.X + BestRect.Y * TextureResolution)
{
BestChartRaster = ChartRaster;
BestOrientation = Orientation;
BestRect = Rect;
if (BestRect.X == 0 && BestRect.Y == 0)
{
// BestRect can't be beat, stop here
break;
}
}
}
else
{
continue;
}
}
}
if (BestOrientation >= 0)
{
// Add chart to layout
OrientChart(Chart, BestOrientation);
LayoutRaster.Alloc(BestRect, BestChartRaster);
Chart.PackingBias.X += BestRect.X;
Chart.PackingBias.Y += BestRect.Y;
}
else
{
// Found no orientation that fit
return false;
}
}
double EndPackCharts = FPlatformTime::Seconds();
UE_LOG(LogMeshDescriptionLayoutUV, Display, TEXT("PackCharts: %s"), *FPlatformTime::PrettyTime(EndPackCharts - BeginPackCharts));
UE_LOG(LogMeshDescriptionLayoutUV, Display, TEXT(" Rasterize: %u"), RasterizeCycles);
UE_LOG(LogMeshDescriptionLayoutUV, Display, TEXT(" Find: %u"), FindCycles);
return true;
}
void FLayoutUV::OrientChart(FMeshChart& Chart, int32 Orientation)
{
switch (Orientation)
{
case 0:
// 0 degrees
Chart.PackingScaleU = FVector2D(Chart.UVScale.X, 0);
Chart.PackingScaleV = FVector2D(0, Chart.UVScale.Y);
Chart.PackingBias = -Chart.MinUV.X * Chart.PackingScaleU - Chart.MinUV.Y * Chart.PackingScaleV + 0.5f;
break;
case 1:
// 0 degrees, flip x
Chart.PackingScaleU = FVector2D(-Chart.UVScale.X, 0);
Chart.PackingScaleV = FVector2D(0, Chart.UVScale.Y);
Chart.PackingBias = -Chart.MaxUV.X * Chart.PackingScaleU - Chart.MinUV.Y * Chart.PackingScaleV + 0.5f;
break;
case 2:
// 90 degrees
Chart.PackingScaleU = FVector2D(0, -Chart.UVScale.X);
Chart.PackingScaleV = FVector2D(Chart.UVScale.Y, 0);
Chart.PackingBias = -Chart.MaxUV.X * Chart.PackingScaleU - Chart.MinUV.Y * Chart.PackingScaleV + 0.5f;
break;
case 3:
// 90 degrees, flip x
Chart.PackingScaleU = FVector2D(0, Chart.UVScale.X);
Chart.PackingScaleV = FVector2D(Chart.UVScale.Y, 0);
Chart.PackingBias = -Chart.MinUV.X * Chart.PackingScaleU - Chart.MinUV.Y * Chart.PackingScaleV + 0.5f;
break;
case 4:
// 180 degrees
Chart.PackingScaleU = FVector2D(-Chart.UVScale.X, 0);
Chart.PackingScaleV = FVector2D(0, -Chart.UVScale.Y);
Chart.PackingBias = -Chart.MaxUV.X * Chart.PackingScaleU - Chart.MaxUV.Y * Chart.PackingScaleV + 0.5f;
break;
case 5:
// 180 degrees, flip x
Chart.PackingScaleU = FVector2D(Chart.UVScale.X, 0);
Chart.PackingScaleV = FVector2D(0, -Chart.UVScale.Y);
Chart.PackingBias = -Chart.MinUV.X * Chart.PackingScaleU - Chart.MaxUV.Y * Chart.PackingScaleV + 0.5f;
break;
case 6:
// 270 degrees
Chart.PackingScaleU = FVector2D(0, Chart.UVScale.X);
Chart.PackingScaleV = FVector2D(-Chart.UVScale.Y, 0);
Chart.PackingBias = -Chart.MinUV.X * Chart.PackingScaleU - Chart.MaxUV.Y * Chart.PackingScaleV + 0.5f;
break;
case 7:
// 270 degrees, flip x
Chart.PackingScaleU = FVector2D(0, -Chart.UVScale.X);
Chart.PackingScaleV = FVector2D(-Chart.UVScale.Y, 0);
Chart.PackingBias = -Chart.MaxUV.X * Chart.PackingScaleU - Chart.MaxUV.Y * Chart.PackingScaleV + 0.5f;
break;
}
}
// Max of 2048x2048 due to precision
// Dilate in 28.4 fixed point. Half pixel dilation is conservative rasterization.
// Dilation same as Minkowski sum of triangle and square.
template< typename TShader, int32 Dilate >
void RasterizeTriangle(TShader& Shader, const FVector2D Points[3], int32 ScissorWidth, int32 ScissorHeight)
{
const FVector2D HalfPixel(0.5f, 0.5f);
FVector2D p0 = Points[0] - HalfPixel;
FVector2D p1 = Points[1] - HalfPixel;
FVector2D p2 = Points[2] - HalfPixel;
// Correct winding
float Facing = (p0.X - p1.X) * (p2.Y - p0.Y) - (p0.Y - p1.Y) * (p2.X - p0.X);
if (Facing < 0.0f)
{
Swap(p0, p2);
}
// 28.4 fixed point
const int32 X0 = (int32)(16.0f * p0.X + 0.5f);
const int32 X1 = (int32)(16.0f * p1.X + 0.5f);
const int32 X2 = (int32)(16.0f * p2.X + 0.5f);
const int32 Y0 = (int32)(16.0f * p0.Y + 0.5f);
const int32 Y1 = (int32)(16.0f * p1.Y + 0.5f);
const int32 Y2 = (int32)(16.0f * p2.Y + 0.5f);
// Bounding rect
int32 MinX = (FMath::Min3(X0, X1, X2) - Dilate + 15) / 16;
int32 MaxX = (FMath::Max3(X0, X1, X2) + Dilate + 15) / 16;
int32 MinY = (FMath::Min3(Y0, Y1, Y2) - Dilate + 15) / 16;
int32 MaxY = (FMath::Max3(Y0, Y1, Y2) + Dilate + 15) / 16;
// Clip to image
MinX = FMath::Clamp(MinX, 0, ScissorWidth);
MaxX = FMath::Clamp(MaxX, 0, ScissorWidth);
MinY = FMath::Clamp(MinY, 0, ScissorHeight);
MaxY = FMath::Clamp(MaxY, 0, ScissorHeight);
// Deltas
const int32 DX01 = X0 - X1;
const int32 DX12 = X1 - X2;
const int32 DX20 = X2 - X0;
const int32 DY01 = Y0 - Y1;
const int32 DY12 = Y1 - Y2;
const int32 DY20 = Y2 - Y0;
// Half-edge constants
int32 C0 = DY01 * X0 - DX01 * Y0;
int32 C1 = DY12 * X1 - DX12 * Y1;
int32 C2 = DY20 * X2 - DX20 * Y2;
// Correct for fill convention
C0 += (DY01 < 0 || (DY01 == 0 && DX01 > 0)) ? 0 : -1;
C1 += (DY12 < 0 || (DY12 == 0 && DX12 > 0)) ? 0 : -1;
C2 += (DY20 < 0 || (DY20 == 0 && DX20 > 0)) ? 0 : -1;
// Dilate edges
C0 += (abs(DX01) + abs(DY01)) * Dilate;
C1 += (abs(DX12) + abs(DY12)) * Dilate;
C2 += (abs(DX20) + abs(DY20)) * Dilate;
for (int32 y = MinY; y < MaxY; y++)
{
for (int32 x = MinX; x < MaxX; x++)
{
// same as Edge1 >= 0 && Edge2 >= 0 && Edge3 >= 0
int32 IsInside;
IsInside = C0 + (DX01 * y - DY01 * x) * 16;
IsInside |= C1 + (DX12 * y - DY12 * x) * 16;
IsInside |= C2 + (DX20 * y - DY20 * x) * 16;
if (IsInside >= 0)
{
Shader.Process(x, y);
}
}
}
}
void FLayoutUV::RasterizeChart(const FMeshChart& Chart, uint32 RectW, uint32 RectH)
{
// Bilinear footprint is -1 to 1 pixels. If packed geometrically, only a half pixel dilation
// would be needed to guarantee all charts were at least 1 pixel away, safe for bilinear filtering.
// Unfortunately, with pixel packing a full 1 pixel dilation is required unless chart edges exactly
// align with pixel centers.
ChartRaster.Clear();
for (uint32 Tri = Chart.FirstTri; Tri < Chart.LastTri; Tri++)
{
FVector2D Points[3];
for (int k = 0; k < 3; k++)
{
const FVector2D& UV = TexCoords[3 * SortedTris[Tri] + k];
Points[k] = UV.X * Chart.PackingScaleU + UV.Y * Chart.PackingScaleV + Chart.PackingBias;
}
RasterizeTriangle< FAllocator2DShader, 16 >(ChartShader, Points, RectW, RectH);
}
if (LayoutVersion >= FMeshDescriptionOperations::ELightmapUVVersion::Segments)
{
ChartRaster.CreateUsedSegments();
}
}
void FLayoutUV::CommitPackedUVs()
{
// If current DstChannel is out of range of the number of UVs defined by the mesh description, change the index count accordingly
const uint32 NumUVs = MeshDescription.VertexInstanceAttributes().GetAttributeIndexCount<FVector2D>(MeshAttribute::VertexInstance::TextureCoordinate);
if (DstChannel >= NumUVs)
{
MeshDescription.VertexInstanceAttributes().SetAttributeIndexCount<FVector2D>(MeshAttribute::VertexInstance::TextureCoordinate, DstChannel + 1);
ensure(false); // not expecting it to get here
}
TVertexInstanceAttributeArray<FVector2D>& VertexUVs = MeshDescription.VertexInstanceAttributes().GetAttributes<FVector2D>(MeshAttribute::VertexInstance::TextureCoordinate, DstChannel);
// Commit chart UVs
for (int32 i = 0; i < Charts.Num(); i++)
{
FMeshChart& Chart = Charts[i];
Chart.PackingScaleU /= TextureResolution;
Chart.PackingScaleV /= TextureResolution;
Chart.PackingBias /= TextureResolution;
for (uint32 Tri = Chart.FirstTri; Tri < Chart.LastTri; Tri++)
{
for (int k = 0; k < 3; k++)
{
uint32 Index = 3 * SortedTris[Tri] + k;
const FVector2D& UV = TexCoords[Index];
const FVertexInstanceID VertexInstanceID(VertexIndexToID[Index]);
VertexUVs[VertexInstanceID] = UV.X * Chart.PackingScaleU + UV.Y * Chart.PackingScaleV + Chart.PackingBias;
}
}
}
}
}