You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
981 lines
28 KiB
C++
981 lines
28 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "Parameterization/MeshUVPacking.h"
|
|
#include "DynamicSubmesh3.h"
|
|
#include "Selections/MeshConnectedComponents.h"
|
|
|
|
#include "Async/Future.h"
|
|
#include "Async/Async.h"
|
|
|
|
#include "Misc/SecureHash.h"
|
|
#include "Allocator2D.h"
|
|
|
|
|
|
FDynamicMeshUVPacker::FDynamicMeshUVPacker(FDynamicMeshUVOverlay* UVOverlayIn)
|
|
{
|
|
UVOverlay = UVOverlayIn;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// local representation of a UV island as a set of triangle indices
|
|
//
|
|
struct FUVIsland
|
|
{
|
|
// Store a unique id so that we can come back to the initial Charts ordering when necessary
|
|
int32 Id;
|
|
|
|
// Set of triangles that make up this UV island. Assumption is this is single connected-component,
|
|
// otherwise multiple islands will be grouped.
|
|
TArray<int32> Triangles;
|
|
|
|
// axis-aligned 2D bounding box min/max
|
|
FVector2d MinUV;
|
|
FVector2d MaxUV;
|
|
|
|
double UVArea;
|
|
FVector2d WorldScale;
|
|
|
|
FVector2d UVScale;
|
|
FVector2d PackingScaleU;
|
|
FVector2d PackingScaleV;
|
|
FVector2d PackingBias;
|
|
};
|
|
|
|
|
|
|
|
|
|
//
|
|
//
|
|
// Chart Packer for UV islands of a FDynamicMesh3
|
|
// This code is a port of the FLayoutUV class/implementation to FDynamicMesh3.
|
|
// The packing strategy is generally the same, however:
|
|
// - additional control over flips has been added
|
|
// - island merging support was removed, input islands must be externally computed
|
|
// - backwards-compatibility paths were removed
|
|
//
|
|
class FDynamicMeshStandardChartPacker
|
|
{
|
|
public:
|
|
FDynamicMesh3* Mesh;
|
|
FDynamicMeshUVOverlay* Overlay;
|
|
|
|
// packing target texture resolution, used to calculate gutter/border size
|
|
uint32 TextureResolution = 512;
|
|
|
|
// if true, UV islands can be mirrored in X and/or Y to improve packing
|
|
bool bAllowFlips = false;
|
|
|
|
double TotalUVArea = 0;
|
|
|
|
// Top-level function, packs input charts into positive-unit-square
|
|
bool FindBestPacking(TArray<FUVIsland>& AllCharts);
|
|
|
|
protected:
|
|
//
|
|
void ScaleCharts(TArray<FUVIsland>& Charts, double UVScale);
|
|
bool PackCharts(TArray<FUVIsland>& Charts, double UVScale, double& OutEfficiency, TAtomic<bool>& bAbort);
|
|
void OrientChart(FUVIsland& Chart, int32 Orientation);
|
|
void RasterizeChart(const FUVIsland& Chart, uint32 RectW, uint32 RectH, FAllocator2D& OutChartRaster);
|
|
};
|
|
|
|
|
|
|
|
bool FDynamicMeshStandardChartPacker::FindBestPacking(TArray<FUVIsland>& Charts)
|
|
{
|
|
if ( (uint32)Charts.Num() > TextureResolution * TextureResolution)
|
|
{
|
|
// More charts than texels
|
|
return false;
|
|
}
|
|
|
|
TotalUVArea = 0.0;
|
|
for (const FUVIsland& Chart : Charts)
|
|
{
|
|
TotalUVArea += Chart.UVArea * Chart.WorldScale.X * Chart.WorldScale.Y;
|
|
}
|
|
|
|
if (TotalUVArea <= 0.0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Cleanup uninitialized values to get a stable input hash
|
|
for (FUVIsland& Chart : Charts)
|
|
{
|
|
Chart.PackingBias = FVector2d::Zero();
|
|
Chart.PackingScaleU = FVector2d::Zero();
|
|
Chart.PackingScaleV = FVector2d::Zero();
|
|
Chart.UVScale = FVector2d::Zero();
|
|
}
|
|
|
|
// Those might require tuning, changing them won't affect the outcome and will maintain backward compatibility
|
|
const int32 MultithreadChartsCountThreshold = 100 * 1000;
|
|
const int32 MultithreadTextureResolutionThreshold = 1000;
|
|
const int32 MultithreadAheadWorkCount = 3;
|
|
|
|
const double LinearSearchStart = 0.5;
|
|
const double LinearSearchStep = 0.5;
|
|
const int32 BinarySearchSteps = 6;
|
|
|
|
double UVScaleFail = TextureResolution * FMathd::Sqrt(1.0 / TotalUVArea);
|
|
double UVScalePass = TextureResolution * FMathd::Sqrt(LinearSearchStart / TotalUVArea);
|
|
|
|
// Store successful charts packing to avoid redoing the final step
|
|
TArray<FUVIsland> LastPassCharts;
|
|
TAtomic<bool> bAbort(false);
|
|
|
|
struct FThreadContext
|
|
{
|
|
TArray<FUVIsland> Charts;
|
|
TFuture<bool> Result;
|
|
double Efficiency = 0.0;
|
|
};
|
|
|
|
TArray<FThreadContext> ThreadContexts;
|
|
|
|
bool bShouldUseMultipleThreads =
|
|
Charts.Num()>= MultithreadChartsCountThreshold &&
|
|
TextureResolution>= MultithreadTextureResolutionThreshold;
|
|
|
|
if (bShouldUseMultipleThreads)
|
|
{
|
|
// Do forward work only when multi-thread activated
|
|
ThreadContexts.SetNum(MultithreadAheadWorkCount);
|
|
}
|
|
|
|
// Linear search for first fit
|
|
double LastEfficiency = 0.0f;
|
|
{
|
|
while (!bAbort)
|
|
{
|
|
// Launch forward work in other threads
|
|
for (int32 Index = 0; Index <ThreadContexts.Num(); ++Index)
|
|
{
|
|
ThreadContexts[Index].Charts = Charts;
|
|
double ThreadUVScale = UVScalePass * FMathd::Pow(LinearSearchStep, (double)(Index + 1));
|
|
ThreadContexts[Index].Result =
|
|
Async(
|
|
EAsyncExecution::ThreadPool,
|
|
[this, &ThreadContexts, &bAbort, ThreadUVScale, Index]()
|
|
{
|
|
return PackCharts(ThreadContexts[Index].Charts, ThreadUVScale, ThreadContexts[Index].Efficiency, bAbort);
|
|
}
|
|
);
|
|
}
|
|
|
|
// Process the first iteration in this thread
|
|
bool bFit = false;
|
|
{
|
|
bFit = PackCharts(Charts, UVScalePass, LastEfficiency, bAbort);
|
|
}
|
|
|
|
// Wait for the work sequentially and cancel everything once we have a first viable solution
|
|
for (int32 Index = 0; Index <ThreadContexts.Num() + 1; ++Index)
|
|
{
|
|
// The first result is not coming from a future
|
|
bFit = Index == 0 ? bFit : ThreadContexts[Index - 1].Result.Get();
|
|
if (bFit && !bAbort)
|
|
{
|
|
// We got a success, cancel other searches
|
|
bAbort = true;
|
|
|
|
if (Index> 0)
|
|
{
|
|
Charts = ThreadContexts[Index - 1].Charts;
|
|
LastEfficiency = ThreadContexts[Index - 1].Efficiency;
|
|
}
|
|
|
|
LastPassCharts = Charts;
|
|
}
|
|
|
|
if (!bAbort)
|
|
{
|
|
UVScaleFail = UVScalePass;
|
|
UVScalePass *= LinearSearchStep;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Binary search for best fit
|
|
{
|
|
bAbort = false;
|
|
for (int32 i = 0; i <BinarySearchSteps; i++)
|
|
{
|
|
double UVScale = 0.5f * (UVScaleFail + UVScalePass);
|
|
|
|
double Efficiency = 0.0f;
|
|
bool bFit = PackCharts(Charts, UVScale, Efficiency, bAbort);
|
|
if (bFit)
|
|
{
|
|
LastPassCharts = Charts;
|
|
double EfficiencyGainPercent = 100.0f * FMathd::Abs(Efficiency - LastEfficiency);
|
|
LastEfficiency = Efficiency;
|
|
|
|
// Early out when we're inside a 1% efficiency range
|
|
if (EfficiencyGainPercent <= 1.0f)
|
|
{
|
|
break;
|
|
}
|
|
|
|
UVScalePass = UVScale;
|
|
}
|
|
else
|
|
{
|
|
UVScaleFail = UVScale;
|
|
}
|
|
}
|
|
}
|
|
|
|
// In case the last step was a failure, restore from last known good computation
|
|
Charts = LastPassCharts;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void FDynamicMeshStandardChartPacker::ScaleCharts( TArray<FUVIsland>& Charts, double UVScale )
|
|
{
|
|
for( int32 i = 0; i <Charts.Num(); i++ )
|
|
{
|
|
FUVIsland& Chart = Charts[i];
|
|
Chart.UVScale = Chart.WorldScale * UVScale;
|
|
}
|
|
|
|
// Unsort the charts to make sure ScaleCharts always return the same ordering
|
|
Algo::IntroSort( Charts, []( const FUVIsland& A, const FUVIsland& B )
|
|
{
|
|
return A.Id < B.Id;
|
|
});
|
|
|
|
// Scale charts such that they all fit and roughly total the same area as before
|
|
#if 1
|
|
double UniformScale = 1.0f;
|
|
for (int i = 0; i < 1000; i++)
|
|
{
|
|
uint32 NumMaxedOut = 0;
|
|
double ScaledUVArea = 0.0f;
|
|
for (int32 ChartIndex = 0; ChartIndex < Charts.Num(); ChartIndex++)
|
|
{
|
|
FUVIsland& Chart = Charts[ChartIndex];
|
|
|
|
FVector2d ChartSize = Chart.MaxUV - Chart.MinUV;
|
|
FVector2d ChartSizeScaled = ChartSize * Chart.UVScale * UniformScale;
|
|
|
|
const double MaxChartEdge = TextureResolution - 1.0f;
|
|
const double LongestChartEdge = FMathd::Max(ChartSizeScaled.X, ChartSizeScaled.Y);
|
|
|
|
const double Epsilon = 0.01f;
|
|
if (LongestChartEdge + Epsilon > MaxChartEdge)
|
|
{
|
|
// Rescale oversized charts to fit
|
|
Chart.UVScale.X = MaxChartEdge / FMathd::Max(ChartSize.X, ChartSize.Y);
|
|
Chart.UVScale.Y = MaxChartEdge / FMathd::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
|
|
double RebalanceScale = UVScale * FMathd::Sqrt(TotalUVArea / ScaledUVArea);
|
|
if (RebalanceScale < 1.01f)
|
|
{
|
|
// Stop if further rebalancing is minor
|
|
break;
|
|
}
|
|
UniformScale = RebalanceScale;
|
|
}
|
|
#endif
|
|
|
|
#if 1
|
|
double NonuniformScale = 1.0f;
|
|
for (int i = 0; i < 1000; i++)
|
|
{
|
|
uint32 NumMaxedOut = 0;
|
|
double ScaledUVArea = 0.0f;
|
|
for (int32 ChartIndex = 0; ChartIndex < Charts.Num(); ChartIndex++)
|
|
{
|
|
FUVIsland& Chart = Charts[ChartIndex];
|
|
|
|
for (int k = 0; k < 2; k++)
|
|
{
|
|
const double MaximumChartSize = TextureResolution - 1.0f;
|
|
const double ChartSize = Chart.MaxUV[k] - Chart.MinUV[k];
|
|
const double ChartSizeScaled = ChartSize * Chart.UVScale[k] * NonuniformScale;
|
|
|
|
const double 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
|
|
double RebalanceScale = UVScale * FMathd::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 FUVIsland& A, const FUVIsland& 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() );
|
|
}
|
|
|
|
|
|
|
|
// Hash function to use FMD5Hash in TMap
|
|
inline uint32 GetTypeHash(const FMD5Hash& Hash)
|
|
{
|
|
uint32* HashAsInt32 = (uint32*)Hash.GetBytes();
|
|
return HashAsInt32[0] ^ HashAsInt32[1] ^ HashAsInt32[2] ^ HashAsInt32[3];
|
|
}
|
|
|
|
bool FDynamicMeshStandardChartPacker::PackCharts(TArray<FUVIsland>& Charts, double UVScale, double& OutEfficiency, TAtomic<bool>& bAbort)
|
|
{
|
|
ScaleCharts(Charts, UVScale);
|
|
|
|
FAllocator2D BestChartRaster(FAllocator2D::EMode::UsedSegments, TextureResolution, TextureResolution, ELightmapUVVersion::Latest);
|
|
FAllocator2D ChartRaster(FAllocator2D::EMode::UsedSegments, TextureResolution, TextureResolution, ELightmapUVVersion::Latest);
|
|
FAllocator2D LayoutRaster(FAllocator2D::EMode::FreeSegments, TextureResolution, TextureResolution, ELightmapUVVersion::Latest);
|
|
|
|
uint64 RasterizeCycles = 0;
|
|
uint64 FindCycles = 0;
|
|
|
|
OutEfficiency = 0.0f;
|
|
LayoutRaster.Clear();
|
|
|
|
// Store the position where we found a spot for each unique raster
|
|
// so we can skip whole sections we know won't work out.
|
|
// This method is obviously more efficient with smaller charts
|
|
// but helps tremendously as the number of charts goes up for
|
|
// the same texture space. This helps counteract the slowdown
|
|
// induced by having more parts to place in the grid and is
|
|
// particularly useful for foliage.
|
|
TMap<FMD5Hash, FVector2d> BestStartPos;
|
|
|
|
// Reduce Insights CPU tracing to once per batch
|
|
const int32 BatchSize = 1024;
|
|
for (int32 ChartIndex = 0; ChartIndex <Charts.Num() && !bAbort.Load(EMemoryOrder::Relaxed); )
|
|
{
|
|
for (int32 BatchIndex = 0; BatchIndex <BatchSize && ChartIndex <Charts.Num() && !bAbort.Load(EMemoryOrder::Relaxed); ++ChartIndex, ++BatchIndex)
|
|
{
|
|
FUVIsland& Chart = Charts[ChartIndex];
|
|
|
|
// Try different orientations and pick best
|
|
int32 BestOrientation = -1;
|
|
FAllocator2D::FRect BestRect = { ~0u, ~0u, ~0u, ~0u };
|
|
|
|
// This version focus on minimal surface area giving fairness to both horizontal and vertical chart placement
|
|
// instead of only taking the pixel offset of the lower left corner into account.
|
|
TFunction<bool(const FAllocator2D::FRect&)> IsBestRect =
|
|
[&BestRect](const FAllocator2D::FRect& Rect)
|
|
{
|
|
return ((Rect.X + Rect.W) + (Rect.Y + Rect.H)) <((BestRect.X + BestRect.W) + (BestRect.Y + BestRect.H));
|
|
};
|
|
|
|
// simpler thing?
|
|
//TFunction<bool(const FAllocator2D::FRect&)> IsBestRect =
|
|
// [this, &BestRect](const FAllocator2D::FRect& Rect)
|
|
//{
|
|
// return Rect.X + Rect.Y * TextureResolution <BestRect.X + BestRect.Y * TextureResolution;
|
|
//};
|
|
|
|
int32 OrientationStep = (bAllowFlips) ? 1 : 2;
|
|
for (int32 Orientation = 0; Orientation < 8; Orientation += OrientationStep)
|
|
{
|
|
// 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( (float)FMathd::Abs(ChartSize.X) + 1.0f);
|
|
Rect.H = FMath::CeilToInt( (float)FMathd::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))
|
|
{
|
|
if (IsBestRect(Rect))
|
|
{
|
|
BestOrientation = Orientation;
|
|
BestRect = Rect;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Orientation % 4 == 1)
|
|
{
|
|
ChartRaster.FlipX(Rect);
|
|
}
|
|
else if (Orientation % 4 == 3)
|
|
{
|
|
ChartRaster.FlipY(Rect);
|
|
}
|
|
else
|
|
{
|
|
RasterizeChart(Chart, Rect.W, Rect.H, ChartRaster);
|
|
}
|
|
|
|
bool bFound = false;
|
|
|
|
// Use the real raster size for optimal placement
|
|
FAllocator2D::FRect RasterRect = Rect;
|
|
RasterRect.W = ChartRaster.GetRasterWidth();
|
|
RasterRect.H = ChartRaster.GetRasterHeight();
|
|
|
|
// Nothing rasterized, returning 0,0 as fast as possible
|
|
// since this is what the actual algorithm is doing but
|
|
// we might have to flag the entire UV map as invalid since
|
|
// charts are going to overlap
|
|
if (RasterRect.H == 0 && RasterRect.W == 0)
|
|
{
|
|
Rect.X = 0;
|
|
Rect.Y = 0;
|
|
bFound = true;
|
|
}
|
|
else
|
|
{
|
|
FMD5Hash RasterMD5 = ChartRaster.GetRasterMD5();
|
|
FVector2d* StartPos = BestStartPos.Find(RasterMD5);
|
|
|
|
if (StartPos)
|
|
{
|
|
RasterRect.X = StartPos->X;
|
|
RasterRect.Y = StartPos->Y;
|
|
}
|
|
|
|
LayoutRaster.ResetStats();
|
|
bFound = LayoutRaster.FindWithSegments(RasterRect, ChartRaster, IsBestRect);
|
|
if (bFound)
|
|
{
|
|
// Store only the best possible position in the hash table so we can start from there for other identical charts
|
|
BestStartPos.Add(RasterMD5, FVector2d(RasterRect.X, RasterRect.Y));
|
|
|
|
// Since the older version stops searching at Width - Rect.W instead of using the raster size,
|
|
// it means a perfect rasterized square of 2,2 won't fit a 2,2 hole at the end of a row if Rect.W = 3.
|
|
// Because of that, we have no choice to worsen our algorithm behavior for backward compatibility.
|
|
|
|
// Once we know the best possible position, we'll continue our search from there with the original
|
|
// rect value if it differs from the raster rect to ensure we get the same result as the old algorithm.
|
|
//if (LayoutVersion <ELightmapUVVersion::Segments2D && (Rect.X != RasterRect.X || Rect.Y != RasterRect.Y))
|
|
//{
|
|
// Rect.X = RasterRect.X;
|
|
// Rect.Y = RasterRect.Y;
|
|
|
|
// bFound = LayoutRaster.FindWithSegments(Rect, ChartRaster, IsBestRect);
|
|
//}
|
|
//else
|
|
//{
|
|
// // We can't copy W and H here as they might be different than what we got initially
|
|
// Rect.X = RasterRect.X;
|
|
// Rect.Y = RasterRect.Y;
|
|
//}
|
|
|
|
// We can't copy W and H here as they might be different than what we got initially
|
|
Rect.X = RasterRect.X;
|
|
Rect.Y = RasterRect.Y;
|
|
|
|
}
|
|
|
|
LayoutRaster.PublishStats(ChartIndex, Orientation, bFound, Rect, BestRect, RasterMD5, IsBestRect);
|
|
}
|
|
|
|
|
|
if (true)
|
|
{
|
|
UE_LOG(LogTemp, Log, TEXT("[LAYOUTUV_TRACE] Chart %d Orientation %d Found = %d Rect = %d,%d,%d,%d\n"), ChartIndex, Orientation, bFound ? 1 : 0, Rect.X, Rect.Y, Rect.W, Rect.H);
|
|
}
|
|
|
|
if (bFound)
|
|
{
|
|
if (IsBestRect(Rect))
|
|
{
|
|
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
|
|
{
|
|
if (true)
|
|
{
|
|
UE_LOG(LogTemp, Log, TEXT("[LAYOUTUV_TRACE] Chart %d Found no orientation that fit\n"), ChartIndex);
|
|
}
|
|
|
|
// Found no orientation that fit
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bAbort)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const uint32 TotalTexels = TextureResolution * TextureResolution;
|
|
const uint32 UsedTexels = LayoutRaster.GetUsedTexels();
|
|
|
|
OutEfficiency = double(UsedTexels) / TotalTexels;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void FDynamicMeshStandardChartPacker::OrientChart(FUVIsland& 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<int32 Dilate>
|
|
void InternalRasterizeTriangle(FAllocator2D& Shader, const FVector2f Points[3], int32 ScissorWidth, int32 ScissorHeight)
|
|
{
|
|
const FVector2f HalfPixel(0.5f, 0.5f);
|
|
FVector2f p0 = Points[0] - HalfPixel;
|
|
FVector2f p1 = Points[1] - HalfPixel;
|
|
FVector2f 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.SetBit(x, y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void FDynamicMeshStandardChartPacker::RasterizeChart(const FUVIsland& Chart, uint32 RectW, uint32 RectH, FAllocator2D& OutChartRaster)
|
|
{
|
|
// 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.
|
|
|
|
OutChartRaster.Clear();
|
|
|
|
for (int32 tid : Chart.Triangles)
|
|
{
|
|
FIndex3i UVTriangle = Overlay->GetTriangle(tid);
|
|
FVector2f Points[3];
|
|
for (int k = 0; k <3; k++)
|
|
{
|
|
FVector2d UV = (FVector2d)Overlay->GetElement(UVTriangle[k]);
|
|
Points[k] = (FVector2f)(UV.X * Chart.PackingScaleU + UV.Y * Chart.PackingScaleV + Chart.PackingBias);
|
|
}
|
|
|
|
InternalRasterizeTriangle<16>(OutChartRaster, Points, RectW, RectH);
|
|
}
|
|
|
|
OutChartRaster.CreateUsedSegments();
|
|
}
|
|
|
|
|
|
|
|
|
|
bool FDynamicMeshUVPacker::StandardPack()
|
|
{
|
|
FDynamicMeshStandardChartPacker Packer;
|
|
|
|
Packer.Mesh = UVOverlay->GetParentMesh();
|
|
Packer.Overlay = UVOverlay;
|
|
|
|
Packer.TextureResolution = this->TextureResolution;
|
|
Packer.bAllowFlips = this->bAllowFlips;
|
|
|
|
FMeshConnectedComponents UVComponents(Packer.Mesh);
|
|
UVComponents.FindConnectedTriangles([&](int32 Triangle0, int32 Triangle1) {
|
|
return UVOverlay->AreTrianglesConnected(Triangle0, Triangle1);
|
|
});
|
|
|
|
int32 NumCharts = UVComponents.Num();
|
|
TArray<FUVIsland> AllCharts;
|
|
AllCharts.SetNum(NumCharts);
|
|
for (int32 ci = 0; ci < NumCharts; ++ci)
|
|
{
|
|
FMeshConnectedComponents::FComponent& Component = UVComponents.GetComponent(ci);
|
|
FUVIsland& Chart = AllCharts[ci];
|
|
|
|
Chart.Id = ci + 1;
|
|
|
|
Chart.Triangles = MoveTemp(Component.Indices);
|
|
|
|
Chart.MinUV = FVector2d(FLT_MAX, FLT_MAX);
|
|
Chart.MaxUV = FVector2d(-FLT_MAX, -FLT_MAX);
|
|
Chart.UVArea = 0.0f;
|
|
Chart.WorldScale = FVector2d::Zero();
|
|
|
|
for (int32 tid : Chart.Triangles)
|
|
{
|
|
FIndex3i Triangle3D = Packer.Mesh->GetTriangle(tid);
|
|
FIndex3i TriangleUV = Packer.Overlay->GetTriangle(tid);
|
|
|
|
FVector3d Positions[3];
|
|
FVector2d UVs[3];
|
|
|
|
for (int k = 0; k < 3; k++)
|
|
{
|
|
Positions[k] = Packer.Mesh->GetVertex(Triangle3D[k]);
|
|
UVs[k] = (FVector2d)UVOverlay->GetElement(TriangleUV[k]);
|
|
|
|
Chart.MinUV.X = FMathd::Min(Chart.MinUV.X, UVs[k].X);
|
|
Chart.MinUV.Y = FMathd::Min(Chart.MinUV.Y, UVs[k].Y);
|
|
Chart.MaxUV.X = FMathd::Max(Chart.MaxUV.X, UVs[k].X);
|
|
Chart.MaxUV.Y = FMathd::Max(Chart.MaxUV.Y, UVs[k].Y);
|
|
}
|
|
|
|
FVector3d Edge1 = Positions[1] - Positions[0];
|
|
FVector3d Edge2 = Positions[2] - Positions[0];
|
|
double Area = 0.5f * (Edge1.Cross(Edge2)).Length();
|
|
|
|
FVector2d EdgeUV1 = UVs[1] - UVs[0];
|
|
FVector2d EdgeUV2 = UVs[2] - UVs[0];
|
|
double UVArea = 0.5f * FMathd::Abs(EdgeUV1.X * EdgeUV2.Y - EdgeUV1.Y * EdgeUV2.X);
|
|
|
|
FVector2d UVLength;
|
|
UVLength.X = (EdgeUV2.Y * Edge1 - EdgeUV1.Y * Edge2).Length();
|
|
UVLength.Y = (-EdgeUV2.X * Edge1 + EdgeUV1.X * Edge2).Length();
|
|
|
|
Chart.WorldScale += UVLength;
|
|
Chart.UVArea += UVArea;
|
|
}
|
|
|
|
Chart.WorldScale /= FMathd::Max(Chart.UVArea, 1e-8f);
|
|
}
|
|
|
|
|
|
bool bPackingFound = Packer.FindBestPacking(AllCharts);
|
|
check(bPackingFound);
|
|
|
|
|
|
// Commit chart UVs
|
|
for (int32 i = 0; i <AllCharts.Num(); i++)
|
|
{
|
|
FUVIsland& Chart = AllCharts[i];
|
|
|
|
Chart.PackingScaleU /= (double)Packer.TextureResolution;
|
|
Chart.PackingScaleV /= (double)Packer.TextureResolution;
|
|
Chart.PackingBias /= (double)Packer.TextureResolution;
|
|
|
|
TSet<int32> IslandElements;
|
|
|
|
for (int32 tid : Chart.Triangles)
|
|
{
|
|
FIndex3i Triangle = UVOverlay->GetTriangle(tid);
|
|
IslandElements.Add(Triangle.A);
|
|
IslandElements.Add(Triangle.B);
|
|
IslandElements.Add(Triangle.C);
|
|
}
|
|
|
|
for (int32 elemid : IslandElements)
|
|
{
|
|
FVector2d UV = (FVector2d)UVOverlay->GetElement(elemid);
|
|
FVector2d TransformedUV = UV.X * Chart.PackingScaleU + UV.Y * Chart.PackingScaleV + Chart.PackingBias;
|
|
UVOverlay->SetElement(elemid, (FVector2f)TransformedUV);
|
|
}
|
|
|
|
}
|
|
|
|
return bPackingFound;
|
|
}
|
|
|
|
|
|
|
|
bool FDynamicMeshUVPacker::StackPack()
|
|
{
|
|
FDynamicMesh3* Mesh = UVOverlay->GetParentMesh();
|
|
|
|
double GutterWidth = (double)GutterSize / (double)TextureResolution;
|
|
|
|
FMeshConnectedComponents UVComponents(Mesh);
|
|
UVComponents.FindConnectedTriangles([&](int32 Triangle0, int32 Triangle1) {
|
|
return UVOverlay->AreTrianglesConnected(Triangle0, Triangle1);
|
|
});
|
|
|
|
int32 NumCharts = UVComponents.Num();
|
|
|
|
// figure out maximum width and height of existing charts
|
|
TArray<FAxisAlignedBox2d> AllIslandBounds;
|
|
AllIslandBounds.SetNum(NumCharts);
|
|
double MaxWidth = 0, MaxHeight = 0;
|
|
for (int32 ci = 0; ci < NumCharts; ++ci)
|
|
{
|
|
FMeshConnectedComponents::FComponent& Component = UVComponents.GetComponent(ci);
|
|
|
|
FAxisAlignedBox2d IslandBounds = FAxisAlignedBox2d::Empty();
|
|
for (int32 tid : Component.Indices)
|
|
{
|
|
FVector2f UVTri[3];
|
|
UVOverlay->GetTriElements(tid, UVTri[0], UVTri[1], UVTri[2]);
|
|
IslandBounds.Contain((FVector2d)UVTri[0]);
|
|
IslandBounds.Contain((FVector2d)UVTri[1]);
|
|
IslandBounds.Contain((FVector2d)UVTri[2]);
|
|
AllIslandBounds[ci] = IslandBounds;
|
|
}
|
|
|
|
MaxWidth = FMathd::Max(IslandBounds.Width(), MaxWidth);
|
|
MaxHeight = FMathd::Max(IslandBounds.Height(), MaxHeight);
|
|
}
|
|
|
|
// figure out uniform scale that will make them all fit
|
|
double TargetWidth = 1.0 - 2 * GutterWidth;
|
|
double TargetHeight = 1.0 - 2 * GutterWidth;
|
|
double WidthScale = TargetWidth / MaxWidth;
|
|
double HeightScale = TargetWidth / MaxHeight;
|
|
double UseUniformScale = (FMathd::Min(WidthScale, HeightScale) > 1) ?
|
|
FMathd::Min(WidthScale, HeightScale) : FMathd::Max(WidthScale, HeightScale);
|
|
|
|
// transform them
|
|
TSet<int32> IslandElements;
|
|
for (int32 ci = 0; ci < NumCharts; ++ci)
|
|
{
|
|
FMeshConnectedComponents::FComponent& Component = UVComponents.GetComponent(ci);
|
|
|
|
IslandElements.Reset();
|
|
FAxisAlignedBox2d IslandBounds = AllIslandBounds[ci];
|
|
for (int32 tid : Component.Indices)
|
|
{
|
|
FIndex3i UVTri = UVOverlay->GetTriangle(tid);
|
|
IslandElements.Add(UVTri[0]);
|
|
IslandElements.Add(UVTri[1]);
|
|
IslandElements.Add(UVTri[2]);
|
|
}
|
|
|
|
for (int32 elemid : IslandElements)
|
|
{
|
|
FVector2d CurUV = (FVector2d)UVOverlay->GetElement(elemid);
|
|
FVector2d NewUV = (CurUV - IslandBounds.Min) * UseUniformScale;
|
|
UVOverlay->SetElement(elemid, (FVector2f)NewUV);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} |