// Copyright Epic Games, Inc. All Rights Reserved. #include "LayoutUV.h" #include "Algo/IntroSort.h" #include "Async/Async.h" #include "DisjointSet.h" #include "OverlappingCorners.h" #include "HAL/PlatformTime.h" #include "Misc/App.h" #include "Misc/SecureHash.h" #include "Modules/ModuleManager.h" IMPLEMENT_MODULE(FDefaultModuleImpl, MeshUtilitiesCommon) DEFINE_LOG_CATEGORY_STATIC(LogLayoutUV, Log, All); #define CHART_JOINING 1 #define NEW_UVS_ARE_SAME UE_THRESH_POINTS_ARE_SAME #define LEGACY_UVS_ARE_SAME UE_THRESH_UVS_ARE_SAME #define UVLAYOUT_THRESH_UVS_ARE_SAME (GetUVEqualityThreshold()) TAtomic FLayoutUV::FindBestPackingCount(0); TAtomic FLayoutUV::FindBestPackingCycles(0); TAtomic FLayoutUV::FindBestPackingEfficiency(0); FLayoutUV::FLayoutUV( IMeshView& InMeshView ) : MeshView( InMeshView ) , LayoutVersion( ELightmapUVVersion::Latest ) , PackedTextureResolution(0) {} /** FIRST PASS: Given a Mesh, build the associated set of charts */ struct FLayoutUV::FChartFinder { FChartFinder(IMeshView& InMeshView, ELightmapUVVersion InLayoutVersion); int32 FindCharts( const FOverlappingCorners& OverlappingCorners, TArray< FVector2f >& TexCoords, TArray< uint32 >& SortedTris, TArray< FMeshChart >& Charts ); private: bool PositionsMatch( uint32 a, uint32 b ) const; bool NormalsMatch( uint32 a, uint32 b ) const; bool UVsMatch( uint32 a, uint32 b ) const; bool VertsMatch( uint32 a, uint32 b ) const; float TriangleUVArea( uint32 Tri ) const; void DisconnectChart( TArray< FMeshChart >& Charts, FMeshChart& Chart, uint32 Side ); float GetUVEqualityThreshold() const; private: IMeshView& MeshView; ELightmapUVVersion LayoutVersion; int32 NextMeshChartId; }; /** SECOND PASS: Given a set of charts, pack them in the UV space */ struct FLayoutUV::FChartPacker { FChartPacker(IMeshView& InMeshView, ELightmapUVVersion InLayoutVersion, uint32 TextureResolution); bool FindBestPacking(const TArray< FVector2f >& TexCoords, const TArray< uint32 >& SortedTris, TArray< FMeshChart >& AllCharts); private: void ScaleCharts( TArray< FMeshChart >& Charts, float UVScale ); bool PackCharts( TArray< FMeshChart >& Charts, float UVScale, const TArray< FVector2f >& TexCoords, const TArray< uint32 >& SortedTris, float& OutEfficiency, TAtomic& bAbort, bool bTrace); void OrientChart( FMeshChart& Chart, int32 Orientation ); void RasterizeChart( const FMeshChart& Chart, const TArray< FVector2f >& TexCoords, const TArray< uint32 >& SortedTris, uint32 RectW, uint32 RectH, FAllocator2D& OutChartRaster ); private: IMeshView& MeshView; ELightmapUVVersion LayoutVersion; uint32 TextureResolution; float TotalUVArea; }; FLayoutUV::FChartPacker::FChartPacker(IMeshView& InMeshView, ELightmapUVVersion InLayoutVersion, uint32 TextureResolution) : MeshView(InMeshView) , LayoutVersion(InLayoutVersion) , TextureResolution(TextureResolution) , TotalUVArea(0.0f) { } int32 FLayoutUV::FindCharts(const FOverlappingCorners& OverlappingCorners) { FChartFinder Finder(MeshView, LayoutVersion); return Finder.FindCharts(OverlappingCorners, MeshTexCoords, MeshSortedTris, MeshCharts); } FLayoutUV::FChartFinder::FChartFinder(IMeshView& InMeshView, ELightmapUVVersion InLayoutVersion) : MeshView(InMeshView) , LayoutVersion(InLayoutVersion) , NextMeshChartId( 0 ) {} int32 FLayoutUV::FChartFinder::FindCharts( const FOverlappingCorners& OverlappingCorners, TArray< FVector2f >& TexCoords, TArray< uint32 >& SortedTris, TArray< FMeshChart >& Charts ) { double Begin = FPlatformTime::Seconds(); uint32 NumIndexes = MeshView.GetNumIndices(); uint32 NumTris = NumIndexes / 3; TArray< int32 > TranslatedMatches; TranslatedMatches.SetNumUninitialized( NumIndexes ); TexCoords.SetNumUninitialized( NumIndexes ); for( uint32 i = 0; i < NumIndexes; i++ ) { TranslatedMatches[i] = -1; TexCoords[i] = MeshView.GetInputTexcoord(i); } // Build disjoint set FDisjointSet DisjointSet( NumTris ); for( uint32 i = 0; i < NumIndexes; i++ ) { const TArray& Overlapping = OverlappingCorners.FindIfOverlapping(i); for (int32 It : Overlapping) { uint32 j = It; 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 FVector2f EdgeUVi = TexCoords[ie] - TexCoords[i]; FVector2f EdgeUVj = TexCoords[je] - TexCoords[j]; // Would these edges match if the charts were translated bool bTranslatedUVMatch = ( EdgeUVi - EdgeUVj ).IsNearlyZero(UVLAYOUT_THRESH_UVS_ARE_SAME); 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; // Build Charts for( uint32 Tri = 0; Tri < NumTris; ) { int32 i = Charts.AddUninitialized(); FMeshChart& Chart = Charts[i]; Chart.Id = NextMeshChartId++; Chart.MinUV = FVector2f( FLT_MAX, FLT_MAX ); Chart.MaxUV = FVector2f( -FLT_MAX, -FLT_MAX ); Chart.UVArea = 0.0f; Chart.WorldScale = FVector2f::ZeroVector; Chart.UVLengthSum = 0.0f; Chart.WorldLengthSum = 0.0f; 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 FVector3f Positions[3]; FVector2f UVs[3]; for( int k = 0; k < 3; k++ ) { uint32 Index = 3 * SortedTris[ Tri ] + k; Positions[k] = MeshView.GetPosition( Index ); 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 ); } FVector3f Edge1 = Positions[1] - Positions[0]; FVector3f Edge2 = Positions[2] - Positions[0]; FVector3f Edge3 = Positions[2] - Positions[1]; FVector2f EdgeUV1 = UVs[1] - UVs[0]; FVector2f EdgeUV2 = UVs[2] - UVs[0]; FVector2f EdgeUV3 = UVs[2] - UVs[1]; float UVArea = 0.5f * FMath::Abs(EdgeUV1.X * EdgeUV2.Y - EdgeUV1.Y * EdgeUV2.X); Chart.UVArea += UVArea; if (LayoutVersion >= ELightmapUVVersion::ScaleByEdgesLength) { float WorldLength = Edge1.Length() + Edge2.Length() + Edge3.Length(); float UVLength = EdgeUV1.Length() + EdgeUV2.Length() + EdgeUV3.Length(); Chart.UVLengthSum += UVLength; Chart.WorldLengthSum += WorldLength; } else { FVector2f UVLength; UVLength.X = (EdgeUV2.Y * Edge1 - EdgeUV1.Y * Edge2).Size(); UVLength.Y = (-EdgeUV2.X * Edge1 + EdgeUV1.X * Edge2).Size(); Chart.WorldScale += UVLength; } } Chart.LastTri = Tri; #if !CHART_JOINING if (LayoutVersion >= ELightmapUVVersion::ScaleByEdgesLength) { if (Chart.UVLengthSum < UE_SMALL_NUMBER) { Chart.WorldScale = FVector2f(1.0f); } else { Chart.WorldScale = FVector2f(Chart.WorldLengthSum / Chart.UVLengthSum); } } else if (LayoutVersion >= ELightmapUVVersion::SmallChartPacking) { Chart.WorldScale /= FMath::Max(Chart.UVArea, UE_SMALL_NUMBER); } else { if (Chart.UVArea > UE_KINDA_SMALL_NUMBER) { Chart.WorldScale /= Chart.UVArea; } else { Chart.WorldScale = FVector2f::ZeroVector; } } #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 ] ]; FVector2f UV0i = TexCoords[ V0i ]; FVector2f UV1i = TexCoords[ V1i ]; FVector2f UV0j = TexCoords[ V0j ]; FVector2f UV1j = TexCoords[ V1j ]; FVector2f EdgeUVi = UV1i - UV0i; FVector2f EdgeUVj = UV1j - UV0j; bool bMirrored = TriangleUVArea( TriI ) * TriangleUVArea( TriJ ) < 0.0f; FVector2f EdgeOffset0 = UV0i - UV1j; FVector2f EdgeOffset1 = UV1i - UV0j; FVector2f 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 ] ) < UVLAYOUT_THRESH_UVS_ARE_SAME; bool bBorderA = FMath::Abs( UV0i[ Axis ] - ( Sign ^ 0 ? Chart.MaxUV[ Axis ] : Chart.MinUV[ Axis ] ) ) < UVLAYOUT_THRESH_UVS_ARE_SAME; bool bBorderB = FMath::Abs( UV0j[ Axis ] - ( Sign ^ 1 ? Chart.MaxUV[ Axis ] : Chart.MinUV[ Axis ] ) ) < UVLAYOUT_THRESH_UVS_ARE_SAME; // FIXME mirrored if( !bAxisAligned || !bBorderA || !bBorderB ) { // Edges weren't on matching rectangle borders continue; } FVector2f CenterA = 0.5f * ( ChartA.MinUV + ChartA.MaxUV ); FVector2f CenterB = 0.5f * ( ChartB.MinUV + ChartB.MaxUV ); FVector2f ExtentA = 0.5f * ( ChartA.MaxUV - ChartA.MinUV ); FVector2f ExtentB = 0.5f * ( ChartB.MaxUV - ChartB.MinUV ); // FIXME mirrored CenterB += Translation; FVector2f CenterDiff = CenterA - CenterB; FVector2f ExtentDiff = ExtentA - ExtentB; FVector2f Separation = ExtentA + ExtentB + CenterDiff * ( Sign ? 1.0f : -1.0f ); bool bCenterMatch = FMath::Abs( CenterDiff[ Axis ^ 1 ] ) < UVLAYOUT_THRESH_UVS_ARE_SAME; bool bExtentMatch = FMath::Abs( ExtentDiff[ Axis ^ 1 ] ) < UVLAYOUT_THRESH_UVS_ARE_SAME; bool bSeparate = FMath::Abs( Separation[ Axis ^ 0 ] ) < UVLAYOUT_THRESH_UVS_ARE_SAME; 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 ); FVector2f 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 >= 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.UVLengthSum += ChartB.UVLengthSum; ChartA.WorldLengthSum += ChartB.WorldLengthSum; ChartA.WorldScale += ChartB.WorldScale; ChartA.UVArea += ChartB.UVArea; ChartB.FirstTri = 0; ChartB.LastTri = 0; ChartB.UVLengthSum = 0.0f; ChartB.WorldLengthSum = 0.0f; ChartB.UVArea = 0.0f; DisconnectChart( Charts, ChartB, Side ^ 2 ); DisconnectChart( Charts, 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(); ) { if( Charts[i].FirstTri == Charts[i].LastTri ) { Charts.RemoveAtSwap(i); } else { i++; } } for( int32 i = 0; i < Charts.Num(); i++ ) { FMeshChart& Chart = Charts[i]; if (LayoutVersion >= ELightmapUVVersion::ScaleByEdgesLength) { if (Chart.UVLengthSum < UE_SMALL_NUMBER) { Chart.WorldScale = FVector2f(1.0f); } else { Chart.WorldScale = FVector2f(Chart.WorldLengthSum / Chart.UVLengthSum); } } else if (LayoutVersion >= ELightmapUVVersion::SmallChartPacking) { Chart.WorldScale /= FMath::Max(Chart.UVArea, UE_SMALL_NUMBER); } else { if (Chart.UVArea > UE_KINDA_SMALL_NUMBER) { Chart.WorldScale /= Chart.UVArea; } else { Chart.WorldScale = FVector2f::ZeroVector; } } } #endif double End = FPlatformTime::Seconds(); UE_LOG(LogLayoutUV, VeryVerbose, TEXT("FindCharts: %s"), *FPlatformTime::PrettyTime(End - Begin) ); return Charts.Num(); } #if UE_EDITOR && (UE_BUILD_DEVELOPMENT || UE_BUILD_DEBUG) static TAutoConsoleVariable CVarLayoutUVTracePackingForInputHash( TEXT("LayoutUV.TracePackingForInputHash"), TEXT(""), TEXT("Activate tracing for the input hash specified in the value.\n"), ECVF_Default); #endif bool FLayoutUV::FChartPacker::FindBestPacking(const TArray< FVector2f >& TexCoords, const TArray< uint32 >& SortedTris, TArray< FMeshChart >& Charts) { if( (uint32)Charts.Num() > TextureResolution * TextureResolution ) { // More charts than texels return false; } TotalUVArea = 0.0f; for (const FMeshChart& Chart : Charts) { TotalUVArea += Chart.UVArea * Chart.WorldScale.X * Chart.WorldScale.Y; } if( TotalUVArea <= 0.0f ) { return false; } uint64 StartCycles = FPlatformTime::Cycles64(); TRACE_CPUPROFILER_EVENT_SCOPE(FChartPacker::FindBestPacking) // Cleanup uninitialized values to get a stable input hash for (FMeshChart& Chart : Charts) { Chart.PackingBias = FVector2f::ZeroVector; Chart.PackingScaleU = FVector2f::ZeroVector; Chart.PackingScaleV = FVector2f::ZeroVector; Chart.UVScale = FVector2f::ZeroVector; } FString InputHash = FMD5::HashBytes((uint8*)Charts.GetData(), Charts.Num() * Charts.GetTypeSize()); #if UE_EDITOR && (UE_BUILD_DEVELOPMENT || UE_BUILD_DEBUG) // When you need to find where an unexpected difference in output hash might come from // after changing the algorithm. You can set this CVar to activate tracing for a particular // input hash. FString InputHashTrace = CVarLayoutUVTracePackingForInputHash.GetValueOnAnyThread().TrimStartAndEnd(); const bool bTrace = InputHashTrace.Len() > 0 && InputHash.StartsWith(InputHashTrace); #else const bool bTrace = false; #endif // 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 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 ); // Store successful charts packing to avoid redoing the final step TArray LastPassCharts; TAtomic bAbort(false); struct FThreadContext { TArray Charts; TFuture Result; float Efficiency = 0.0f; }; TArray ThreadContexts; bool bShouldUseMultipleThreads = FApp::ShouldUseThreadingForPerformance() && !bTrace && Charts.Num() >= MultithreadChartsCountThreshold && TextureResolution >= MultithreadTextureResolutionThreshold; if ( bShouldUseMultipleThreads ) { // Do forward work only when multi-thread activated ThreadContexts.SetNum(MultithreadAheadWorkCount); } // Linear search for first fit float LastEfficiency = 0.0f; { TRACE_CPUPROFILER_EVENT_SCOPE(LinearSearch); while(!bAbort) { // Launch forward work in other threads for (int32 Index = 0; Index < ThreadContexts.Num(); ++Index) { ThreadContexts[Index].Charts = Charts; float ThreadUVScale = UVScalePass * FMath::Pow(LinearSearchStep, Index + 1); ThreadContexts[Index].Result = Async( EAsyncExecution::ThreadPool, [this, &ThreadContexts, &SortedTris, &TexCoords, &bAbort, ThreadUVScale, Index]() { TRACE_CPUPROFILER_EVENT_SCOPE(SearchStep); return PackCharts(ThreadContexts[Index].Charts, ThreadUVScale, TexCoords, SortedTris, ThreadContexts[Index].Efficiency, bAbort, false); } ); } if (bTrace) { UE_LOG(LogLayoutUV, Log, TEXT("[LAYOUTUV_TRACE] Scale %f"), UVScalePass); } // Process the first iteration in this thread bool bFit = false; { TRACE_CPUPROFILER_EVENT_SCOPE(SearchStep); bFit = PackCharts(Charts, UVScalePass, TexCoords, SortedTris, LastEfficiency, bAbort, bTrace); } // 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 { TRACE_CPUPROFILER_EVENT_SCOPE(BinarySearch); bAbort = false; for( int32 i = 0; i < BinarySearchSteps; i++ ) { TRACE_CPUPROFILER_EVENT_SCOPE(SearchStep); float UVScale = 0.5f * ( UVScaleFail + UVScalePass ); if (bTrace) { UE_LOG(LogLayoutUV, Log, TEXT("[LAYOUTUV_TRACE] Scale %f"), UVScale); } float Efficiency = 0.0f; bool bFit = PackCharts(Charts, UVScale, TexCoords, SortedTris, Efficiency, bAbort, bTrace); if( bFit ) { LastPassCharts = Charts; float EfficiencyGainPercent = 100.0f * FMath::Abs(Efficiency - LastEfficiency); LastEfficiency = Efficiency; // Early out when we're inside a 1% efficiency range if (LayoutVersion >= ELightmapUVVersion::Segments2D && EfficiencyGainPercent <= 1.0f) { break; } UVScalePass = UVScale; } else { UVScaleFail = UVScale; } } } if (LayoutVersion < ELightmapUVVersion::ScaleChartsOrderingFix) { // Early versions applied a sort that was determinist // but dependent on earlier sorts. Since we strive to maintain // backward compatibility of the UV layout to avoid screwing // with already backed static lighting, we must apply a final // scaling and packing that will reuse the last step's ordering // whether it was a failure or not. PackCharts(Charts, UVScalePass, TexCoords, SortedTris, LastEfficiency, bAbort, bTrace); } else { // In case the last step was a failure, restore from last known good computation Charts = LastPassCharts; } FString OutputHash = FMD5::HashBytes((uint8*)Charts.GetData(), Charts.Num() * Charts.GetTypeSize()); // Increase verbosity level to use this for packing results validation when modifying code UE_LOG(LogLayoutUV, Verbose, TEXT("FindBestPacking (Input Data MD5: %s, Output Data MD5: %s, LayoutVersion: %d, Efficiency: %0.2f %%)"), *InputHash, *OutputHash, int(LayoutVersion), LastEfficiency*100); static TAtomic Count(0); static TAtomic TotalCycles(0); static TAtomic Efficiency(0); FindBestPackingCount++; FindBestPackingEfficiency += LastEfficiency*100000; FindBestPackingCycles += FPlatformTime::Cycles64() - StartCycles; return true; } void FLayoutUV::ResetStats() { FindBestPackingCount = 0; FindBestPackingEfficiency = 0; FindBestPackingCycles = 0; } void FLayoutUV::LogStats() { UE_LOG(LogLayoutUV, Log, TEXT("FindBestPacking (Total Time: %s, Avg Efficiency: %f)"), *FPlatformTime::PrettyTime(FPlatformTime::ToSeconds64(FindBestPackingCycles.Load())), double(FindBestPackingEfficiency.Load()) / (FindBestPackingCount.Load()*1000)); } void FLayoutUV::FChartPacker::ScaleCharts( TArray< FMeshChart >& Charts, float UVScale ) { for( int32 i = 0; i < Charts.Num(); i++ ) { FMeshChart& Chart = Charts[i]; Chart.UVScale = Chart.WorldScale * UVScale; } if ( LayoutVersion >= 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]; FVector2f ChartSize = Chart.MaxUV - Chart.MinUV; FVector2f 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 FVector2f ChartRectA = ( A.MaxUV - A.MinUV ) * A.UVScale; FVector2f 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 FLayoutUV::FChartPacker::PackCharts(TArray< FMeshChart >& Charts, float UVScale, const TArray< FVector2f >& TexCoords, const TArray< uint32 >& SortedTris, float& OutEfficiency, TAtomic& bAbort, bool bTrace) { ScaleCharts( Charts, UVScale ); TRACE_CPUPROFILER_EVENT_SCOPE(FChartPacker::PackCharts) FAllocator2D BestChartRaster(FAllocator2D::EMode::UsedSegments, TextureResolution, TextureResolution, LayoutVersion); FAllocator2D ChartRaster (FAllocator2D::EMode::UsedSegments, TextureResolution, TextureResolution, LayoutVersion); FAllocator2D LayoutRaster (FAllocator2D::EMode::FreeSegments, TextureResolution, TextureResolution, LayoutVersion); uint64 RasterizeCycles = 0; uint64 FindCycles = 0; double BeginPackCharts = FPlatformTime::Seconds(); 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 BestStartPos; // Reduce Insights CPU tracing to once per batch const int32 BatchSize = 1024; for( int32 ChartIndex = 0; ChartIndex < Charts.Num() && !bAbort.Load(EMemoryOrder::Relaxed);) { TRACE_CPUPROFILER_EVENT_SCOPE(ChartBatch); for( int32 BatchIndex = 0; BatchIndex < BatchSize && ChartIndex < Charts.Num() && !bAbort.Load(EMemoryOrder::Relaxed); ++ChartIndex, ++BatchIndex) { FMeshChart& Chart = Charts[ChartIndex]; // Try different orientations and pick best int32 BestOrientation = -1; FAllocator2D::FRect BestRect = { ~0u, ~0u, ~0u, ~0u }; // Refactored BestRect comparison code in one place so this can be customized per version if needed TFunction IsBestRect; if ( LayoutVersion >= ELightmapUVVersion::OptimalSurfaceArea ) { // 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. IsBestRect = [&BestRect](const FAllocator2D::FRect& Rect) { return ((Rect.X+Rect.W) + (Rect.Y+Rect.H)) < ((BestRect.X+BestRect.W) + (BestRect.Y+BestRect.H)); }; } else { IsBestRect = [this, &BestRect](const FAllocator2D::FRect& Rect) { return Rect.X + Rect.Y * TextureResolution < BestRect.X + BestRect.Y * TextureResolution; }; } for( int32 Orientation = 0; Orientation < 8; Orientation++ ) { // TODO If any dimension is less than 1 pixel shrink dimension to zero OrientChart( Chart, Orientation); FVector2f 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 ) ) { if( IsBestRect(Rect) ) { BestOrientation = Orientation; BestRect = Rect; } } else { continue; } } else { if ( LayoutVersion >= ELightmapUVVersion::Segments && Orientation % 4 == 1 ) { ChartRaster.FlipX( Rect ); } else if ( LayoutVersion >= ELightmapUVVersion::Segments && Orientation % 4 == 3 ) { ChartRaster.FlipY( Rect ); } else { int32 BeginRasterize = FPlatformTime::Cycles(); RasterizeChart( Chart, TexCoords, SortedTris, Rect.W , Rect.H, ChartRaster); RasterizeCycles += FPlatformTime::Cycles() - BeginRasterize; } bool bFound = false; uint32 BeginFind = FPlatformTime::Cycles(); if ( LayoutVersion == ELightmapUVVersion::BitByBit ) { bFound = LayoutRaster.FindBitByBit( Rect, ChartRaster ); } else if ( LayoutVersion >= ELightmapUVVersion::Segments ) { // 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(); FVector2f* 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, FVector2f(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; } } LayoutRaster.PublishStats(ChartIndex, Orientation, bFound, Rect, BestRect, RasterMD5, IsBestRect); } } FindCycles += FPlatformTime::Cycles() - BeginFind; if (bTrace) { UE_LOG(LogLayoutUV, 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 (bTrace) { UE_LOG(LogLayoutUV, 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 = float( UsedTexels ) / TotalTexels; double EndPackCharts = FPlatformTime::Seconds(); UE_LOG(LogLayoutUV, VeryVerbose, TEXT("PackCharts: %s"), *FPlatformTime::PrettyTime(EndPackCharts - BeginPackCharts)); UE_LOG(LogLayoutUV, VeryVerbose, TEXT(" Rasterize: %llu"), RasterizeCycles); UE_LOG(LogLayoutUV, VeryVerbose, TEXT(" Find: %llu"), FindCycles); return true; } void FLayoutUV::FChartPacker::OrientChart( FMeshChart& Chart, int32 Orientation ) { switch( Orientation ) { case 0: // 0 degrees Chart.PackingScaleU = FVector2f( Chart.UVScale.X, 0 ); Chart.PackingScaleV = FVector2f( 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 = FVector2f( -Chart.UVScale.X, 0 ); Chart.PackingScaleV = FVector2f( 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 = FVector2f( 0, -Chart.UVScale.X ); Chart.PackingScaleV = FVector2f( 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 = FVector2f( 0, Chart.UVScale.X ); Chart.PackingScaleV = FVector2f( 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 = FVector2f( -Chart.UVScale.X, 0 ); Chart.PackingScaleV = FVector2f( 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 = FVector2f( Chart.UVScale.X, 0 ); Chart.PackingScaleV = FVector2f( 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 = FVector2f( 0, Chart.UVScale.X ); Chart.PackingScaleV = FVector2f( -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 = FVector2f( 0, -Chart.UVScale.X ); Chart.PackingScaleV = FVector2f( -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 RasterizeTriangle( 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 FLayoutUV::FChartPacker::RasterizeChart( const FMeshChart& Chart, const TArray< FVector2f >& TexCoords, const TArray< uint32 >& SortedTris, 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( uint32 Tri = Chart.FirstTri; Tri < Chart.LastTri; Tri++ ) { FVector2f Points[3]; for ( int k = 0; k < 3; k++ ) { const FVector2f& UV = TexCoords[ 3 * SortedTris[ Tri ] + k ]; Points[k] = UV.X * Chart.PackingScaleU + UV.Y * Chart.PackingScaleV + Chart.PackingBias; } RasterizeTriangle< 16 >( OutChartRaster, Points, RectW, RectH ); } if ( LayoutVersion >= ELightmapUVVersion::Segments ) { OutChartRaster.CreateUsedSegments(); } } bool FLayoutUV::FindBestPacking(uint32 InTextureResolution) { FChartPacker Packer(MeshView, LayoutVersion, InTextureResolution); bool bPackingFound = Packer.FindBestPacking(MeshTexCoords, MeshSortedTris, MeshCharts); PackedTextureResolution = bPackingFound ? InTextureResolution : 0; return bPackingFound; } void FLayoutUV::CommitPackedUVs() { if (PackedTextureResolution == 0) { return; } TRACE_CPUPROFILER_EVENT_SCOPE(FLayoutUV::CommitPackedUVs) // Alloc new UV channel MeshView.InitOutputTexcoords(MeshTexCoords.Num()); // Commit chart UVs for( int32 i = 0; i < MeshCharts.Num(); i++ ) { FMeshChart& Chart = MeshCharts[i]; Chart.PackingScaleU /= PackedTextureResolution; Chart.PackingScaleV /= PackedTextureResolution; Chart.PackingBias /= PackedTextureResolution; for( uint32 Tri = Chart.FirstTri; Tri < Chart.LastTri; Tri++ ) { for( int k = 0; k < 3; k++ ) { uint32 Index = 3 * MeshSortedTris[ Tri ] + k; const FVector2f& UV = MeshTexCoords[ Index ]; FVector2f TransformedUV = UV.X * Chart.PackingScaleU + UV.Y * Chart.PackingScaleV + Chart.PackingBias; MeshView.SetOutputTexcoord(Index, TransformedUV); } } } } inline bool FLayoutUV::FChartFinder::PositionsMatch( uint32 a, uint32 b ) const { return ( MeshView.GetPosition(a) - MeshView.GetPosition(b) ).IsNearlyZero( THRESH_POINTS_ARE_SAME ); } inline bool FLayoutUV::FChartFinder::NormalsMatch( uint32 a, uint32 b ) const { return ( MeshView.GetNormal(a) - MeshView.GetNormal(b) ).IsNearlyZero( THRESH_NORMALS_ARE_SAME ); } inline bool FLayoutUV::FChartFinder::UVsMatch( uint32 a, uint32 b ) const { return ( MeshView.GetInputTexcoord(a) - MeshView.GetInputTexcoord(b) ).IsNearlyZero(UVLAYOUT_THRESH_UVS_ARE_SAME); } inline bool FLayoutUV::FChartFinder::VertsMatch( uint32 a, uint32 b ) const { return PositionsMatch( a, b ) && UVsMatch( a, b ); } // Signed UV area inline float FLayoutUV::FChartFinder::TriangleUVArea( uint32 Tri ) const { FVector2f UVs[3]; for( int k = 0; k < 3; k++ ) { UVs[k] = MeshView.GetInputTexcoord(3 * Tri + k); } FVector2f EdgeUV1 = UVs[1] - UVs[0]; FVector2f EdgeUV2 = UVs[2] - UVs[0]; return 0.5f * ( EdgeUV1.X * EdgeUV2.Y - EdgeUV1.Y * EdgeUV2.X ); } inline void FLayoutUV::FChartFinder::DisconnectChart( TArray< FMeshChart >& Charts, FMeshChart& Chart, uint32 Side ) { if( Chart.Join[ Side ] != -1 ) { Charts[ Chart.Join[ Side ] ].Join[ Side ^ 1 ] = -1; Chart.Join[ Side ] = -1; } } inline float FLayoutUV::FChartFinder::GetUVEqualityThreshold() const { return LayoutVersion >= ELightmapUVVersion::SmallChartPacking ? NEW_UVS_ARE_SAME : LEGACY_UVS_ARE_SAME; }