Files
UnrealEngine/Engine/Source/Runtime/Renderer/Private/Nanite/TessellationTable.cpp
2025-05-18 13:04:45 +08:00

868 lines
26 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "TessellationTable.h"
#include "NaniteDefinitions.h"
#include "Misc/Paths.h"
#include "HAL/FileManager.h"
#define NANITE_BUILD_TESSELLATION_TABLE 0
#if NANITE_BUILD_TESSELLATION_TABLE
#include "DynamicMesh/DynamicMesh3.h"
#include "TriangleUtil.h"
#include "Misc/OutputDeviceRedirector.h"
#ifdef _MSC_VER
#pragma warning(disable : 6385)
#endif
using namespace UE::Geometry;
#endif
namespace Nanite
{
FTessellationTable::FTessellationTable()
{
/*
NumPatterns = (MaxTessFactor + 2) choose 3
NumPatterns = 1/6 * N(N+1)(N+2)
= 816
*/
FString FilePath = FPaths::EngineContentDir() / TEXT("Renderer/TessellationTable.bin");
#if NANITE_BUILD_TESSELLATION_TABLE
uint32 Time0 = FPlatformTime::Cycles();
OffsetTable.AddZeroed( 2 * NANITE_TESSELLATION_TABLE_PO2_SIZE * NANITE_TESSELLATION_TABLE_PO2_SIZE * NANITE_TESSELLATION_TABLE_PO2_SIZE );
// TessFactors in descending order to reduce size of table.
// Regular tessellation table
for( uint32 TessFactorZ = 1; TessFactorZ <= NANITE_TESSELLATION_TABLE_SIZE; TessFactorZ++ )
{
for( uint32 TessFactorY = TessFactorZ; TessFactorY <= NANITE_TESSELLATION_TABLE_SIZE; TessFactorY++ )
{
for( uint32 TessFactorX = TessFactorY; TessFactorX <= NANITE_TESSELLATION_TABLE_SIZE; TessFactorX++ )
{
FIntVector TessFactors( TessFactorX, TessFactorY, TessFactorZ );
AddPatch( TessFactors );
//ConstrainToCacheWindow();
WriteSVG( TessFactors );
AddToVertsAndIndices( false, TessFactors );
}
}
}
// Immediate-mode tessellation table
for( uint32 TessFactorZ = 1; TessFactorZ <= NANITE_TESSELLATION_TABLE_IMMEDIATE_SIZE; TessFactorZ++ )
{
for( uint32 TessFactorY = TessFactorZ; TessFactorY <= NANITE_TESSELLATION_TABLE_IMMEDIATE_SIZE; TessFactorY++ )
{
for( uint32 TessFactorX = TessFactorY; TessFactorX <= NANITE_TESSELLATION_TABLE_IMMEDIATE_SIZE; TessFactorX++ )
{
FIntVector TessFactors( TessFactorX, TessFactorY, TessFactorZ );
// TODO: Reuse already generated data instead of generating it again?
AddPatch( TessFactors );
WriteSVG( TessFactors );
const uint32 NumTrisBefore = Indexes.Num();
ConstrainToCacheWindow();
ConstrainForImmediateTessellation();
const uint32 NumTrisAfter = Indexes.Num();
check( NumTrisAfter <= NumTrisBefore + 2 ); // Two degenerate triangles needed for first triangle, so this is optimal
AddToVertsAndIndices( true, TessFactors );
}
}
}
uint32 Time1 = FPlatformTime::Cycles();
GLog->Logf( TEXT("TessellationTable [%.2fms]"), FPlatformTime::ToMilliseconds( Time1 - Time0 ) );
TUniquePtr< FArchive > Ar( IFileManager::Get().CreateFileWriter( *FilePath ) );
#else
TUniquePtr< FArchive > Ar( IFileManager::Get().CreateFileReader( *FilePath ) );
#endif
*Ar << OffsetTable;
*Ar << VertsAndIndexes;
}
int32 FTessellationTable::GetPattern( FIntVector TessFactors ) const
{
checkSlow( 0 < TessFactors[0] && TessFactors[0] <= int32(NANITE_TESSELLATION_TABLE_SIZE) );
checkSlow( 0 < TessFactors[1] && TessFactors[1] <= int32(NANITE_TESSELLATION_TABLE_SIZE) );
checkSlow( 0 < TessFactors[2] && TessFactors[2] <= int32(NANITE_TESSELLATION_TABLE_SIZE) );
if( TessFactors[0] < TessFactors[1] ) Swap( TessFactors[0], TessFactors[1] );
if( TessFactors[0] < TessFactors[2] ) Swap( TessFactors[0], TessFactors[2] );
if( TessFactors[1] < TessFactors[2] ) Swap( TessFactors[1], TessFactors[2] );
return
( TessFactors[0] - 1 ) +
( TessFactors[1] - 1 ) * NANITE_TESSELLATION_TABLE_PO2_SIZE +
( TessFactors[2] - 1 ) * NANITE_TESSELLATION_TABLE_PO2_SIZE * NANITE_TESSELLATION_TABLE_PO2_SIZE;
}
FIntVector FTessellationTable::GetBarycentrics( uint32 Vert ) const
{
FIntVector Barycentrics;
Barycentrics.X = Vert & 0xffff;
Barycentrics.Y = Vert >> 16;
Barycentrics.Z = BarycentricMax - Barycentrics.X - Barycentrics.Y;
return Barycentrics;
}
#if NANITE_BUILD_TESSELLATION_TABLE
void Cache()
{
#if 0
using namespace UE::DerivedData;
FString DerivedDataKey( TEXT("FTessellationTable-0") );
FCacheKey CacheKey;
CacheKey.Bucket = FCacheBucket( TEXT("FTessellationTable") );
CacheKey.Hash = FIoHash::HashBuffer( MakeMemoryView( FTCHARToUTF8( DerivedDataKey ) ) );
// Check if the data already exists in DDC
FSharedBuffer Data;
{
FCacheGetValueRequest Request;
Request.Name = FString("Get-FTessellationTable");
Request.Key = CacheKey;
Request.Policy = ECachePolicy::Local;
FRequestOwner RequestOwner( EPriority::Blocking );
GetCache().GetValue( MakeArrayView( &Request, 1 ), RequestOwner,
[&]( FCacheGetValueResponse&& Response )
{
if( Response.Status == EStatus::Ok )
{
Data = Response.Value.GetData().Decompress();
}
} );
RequestOwner.Wait();
}
if( !Data.IsNull() )
{
FMemoryReaderView Ar( Data.GetView(), /*bIsPersistent=*/ true );
Serialize( Ar, Owner, /*bCooked=*/ false );
}
else
{
// DDC lookup failed! Build the data again.
const bool bBuiltSuccessfully = Build(Owner, SourceData);
FMemoryWriter Ar( 0, /*bIsPersistent=*/ true );
Serialize( Ar, Owner, /*bCooked=*/ false );
FCachePutValueRequest Request;
Request.Name = FString("Put-FTessellationTable");
Request.Key = CacheKey;
Request.Value = FValue::Compress( FSharedBuffer::MakeView( Ar.GetData(), Ar.TotalSize() ) );
Request.Policy = ECachePolicy::Local;
FRequestOwner RequestOwner( EPriority::Blocking );
GetCache().PutValue( MakeArrayView( &Request, 1 ), RequestOwner );
RequestOwner.Wait();
}
#endif
}
// Snap to exact TessFactor at the edges
void FTessellationTable::SnapAtEdges( FIntVector& Barycentrics, const FIntVector& TessFactors ) const
{
for( uint32 i = 0; i < 3; i++ )
{
const uint32 e0 = i;
const uint32 e1 = (1 << e0) & 3;
// Am I on this edge?
if( Barycentrics[ e0 ] + Barycentrics[ e1 ] == BarycentricMax )
{
// Snap toward min barycentric means snapping mirrors. Adjacent patches will thus match.
uint32 MinIndex = Barycentrics[ e0 ] < Barycentrics[ e1 ] ? e0 : e1;
uint32 MaxIndex = Barycentrics[ e0 ] >= Barycentrics[ e1 ] ? e0 : e1;
// Fixed point round
uint32 Snapped = ( Barycentrics[ MinIndex ] * TessFactors[i] + (BarycentricMax / 2) - 1 ) & ~( BarycentricMax - 1 );
Barycentrics[ MinIndex ] = Snapped / TessFactors[i];
Barycentrics[ MaxIndex ] = BarycentricMax - Barycentrics[ MinIndex ];
}
}
}
class FTessellatedPatch
{
public:
FIntVector TessFactors;
FDynamicMesh3 Mesh;
public:
FTessellatedPatch( const FIntVector& InTessFactors );
private:
void Uniform();
void Remesh();
void ProcessEdge( int EdgeIndex );
TArray< FVector3d > Relaxed;
};
FTessellatedPatch::FTessellatedPatch( const FIntVector& InTessFactors )
: TessFactors( InTessFactors )
{
if( TessFactors[0] == TessFactors[2] )
Uniform();
else
Remesh();
}
void FTessellatedPatch::Uniform()
{
const uint32 NumVerts = ( ( TessFactors[0] + 1 ) * ( TessFactors[0] + 2 ) ) / 2;
const uint32 NumTris = TessFactors[0] * TessFactors[0];
/*
Starts from top point. Adds rows of verts and corresponding rows of tri strips.
|\
row |\|\
|\|\|\
column
*/
for( uint32 VertIndex = 0; VertIndex < NumVerts; VertIndex++ )
{
// Find largest tessellation with NumVerts <= VertIndex. These are the preceding verts before this row.
uint32 VertRow = FMath::FloorToInt( FMath::Sqrt( VertIndex * 8.0f + 1.0f ) * 0.5f - 0.5f );
uint32 VertCol = VertIndex - ( VertRow * ( VertRow + 1 ) ) / 2;
FVector3d Barycentrics;
Barycentrics[0] = TessFactors[0] - VertRow;
Barycentrics[1] = VertCol;
Barycentrics[2] = VertRow - VertCol;
Barycentrics /= TessFactors[0];
Mesh.AppendVertex( Barycentrics );
}
for( uint32 TriIndex = 0; TriIndex < NumTris; TriIndex++ )
{
// Find largest tessellation with NumTris <= TriIndex. These are the preceding tris before this row.
uint32 TriRow = FMath::FloorToInt( FMath::Sqrt( (float)TriIndex ) );
uint32 TriCol = TriIndex - TriRow * TriRow;
/*
Vert order:
0 0__1
|\ \ |
| \ \ | <= flip triangle
|__\ \|
2 1 2
*/
uint32 FlipTri = TriCol & 1;
uint32 VertCol = TriCol >> 1;
uint32 VertRowCol[3][2] =
{
{ TriRow, VertCol },
{ TriRow + 1, VertCol + 1 },
{ TriRow + 1, VertCol },
};
VertRowCol[1][0] -= FlipTri;
VertRowCol[2][1] += FlipTri;
FIndex3i Triangle;
for( int Corner = 0; Corner < 3; Corner++ )
{
uint32 Row = VertRowCol[ Corner ][0];
uint32 Col = VertRowCol[ Corner ][1];
Triangle[ Corner ] = Col + ( Row * ( Row + 1 ) ) / 2;
}
Mesh.AppendTriangle( Triangle );
}
}
void FTessellatedPatch::Remesh()
{
FIndex3i Triangle;
Triangle[0] = Mesh.AppendVertex( FVector3d( 1, 0, 0 ) );
Triangle[1] = Mesh.AppendVertex( FVector3d( 0, 1, 0 ) );
Triangle[2] = Mesh.AppendVertex( FVector3d( 0, 0, 1 ) );
Mesh.AppendTriangle( Triangle );
const int RemeshIterations = 64;
for (int k = 0; k < RemeshIterations; ++k)
{
{
int MaxEdgeID = Mesh.MaxEdgeID();
int StartIndex = k % MaxEdgeID;
int EdgeIndex = StartIndex;
do
{
if( Mesh.IsEdge( EdgeIndex ) )
ProcessEdge( EdgeIndex );
// Iterate in random order
const int ModuloPrime = 31337;
EdgeIndex = ( EdgeIndex + ModuloPrime ) % MaxEdgeID;
} while( EdgeIndex != StartIndex );
}
Relaxed.SetNum( Mesh.MaxVertexID(), EAllowShrinking::No );
for( int VertexIndex : Mesh.VertexIndicesItr() )
{
FVector3d v = Mesh.GetVertex( VertexIndex );
if( !Mesh.IsBoundaryVertex( VertexIndex ) )
{
Mesh.GetVtxOneRingCentroid( VertexIndex, v );
}
Relaxed[ VertexIndex ] = v;
}
for( int VertexIndex : Mesh.VertexIndicesItr() )
{
Mesh.SetVertex( VertexIndex, Relaxed[ VertexIndex ] );
}
}
}
void FTessellatedPatch::ProcessEdge( int EdgeIndex )
{
// MinEdgeLength < MaxEdgeLength/2
const float MinEdgeLength = 0.66f;
const float MaxEdgeLength = 1.33f;
FDynamicMesh3::FEdge Edge = Mesh.GetEdge( EdgeIndex );
FVector3d v0 = Mesh.GetVertex( Edge.Vert[0] );
FVector3d v1 = Mesh.GetVertex( Edge.Vert[1] );
float EdgeLengthSqr = Barycentric::LengthSquared( FVector3f(v0), FVector3f(v1), FVector3f( TessFactors * TessFactors ) );
bool bBoundary0 = Mesh.IsBoundaryVertex( Edge.Vert[0] );
bool bBoundary1 = Mesh.IsBoundaryVertex( Edge.Vert[1] );
bool bBoundaryEdge = Mesh.IsBoundaryEdge( EdgeIndex );
if( ( bBoundaryEdge || !bBoundary0 || !bBoundary1 ) && EdgeLengthSqr < MinEdgeLength * MinEdgeLength )
{
int iKeep = Edge.Vert[0];
int iRemove = Edge.Vert[1];
double collapse_t = 0.5;
if( bBoundary0 )
{
iKeep = Edge.Vert[0];
iRemove = Edge.Vert[1];
collapse_t = 0;
}
if( bBoundary1 )
{
iKeep = Edge.Vert[1];
iRemove = Edge.Vert[0];
collapse_t = 0;
}
FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
EMeshResult Result = Mesh.CollapseEdge( iKeep, iRemove, collapse_t, CollapseInfo );
if( Result == EMeshResult::Ok )
return;
}
if( !bBoundaryEdge )
{
FIndex2i Opposing = Mesh.GetEdgeOpposingV( EdgeIndex );
FVector3d o0 = Mesh.GetVertex( Opposing[0] );
FVector3d o1 = Mesh.GetVertex( Opposing[1] );
float FlipDistSqr = Barycentric::LengthSquared( FVector3f(o0), FVector3f(o1), FVector3f( TessFactors * TessFactors ) );
bool bFlipEdge = FlipDistSqr < 0.9f * EdgeLengthSqr;
if( bFlipEdge )
{
FDynamicMesh3::FEdgeFlipInfo FlipInfo;
EMeshResult Result = Mesh.FlipEdge( EdgeIndex, FlipInfo );
if( Result == EMeshResult::Ok )
return;
}
}
if( EdgeLengthSqr > MaxEdgeLength * MaxEdgeLength )
{
float SplitT = 0.5f;
if( bBoundaryEdge )
{
float EdgeLength = FMath::Sqrt( EdgeLengthSqr );
float NumEdgeSplits = FMath::Floor( EdgeLength + 0.5f );
float HalfSplit = FMath::Floor( NumEdgeSplits / 2.0f );
SplitT = HalfSplit / NumEdgeSplits;
}
FDynamicMesh3::FEdgeSplitInfo SplitInfo;
EMeshResult Result = Mesh.SplitEdge( EdgeIndex, SplitInfo, SplitT );
if( Result == EMeshResult::Ok )
return;
}
}
void FTessellationTable::AddPatch( const FIntVector& TessFactors )
{
FTessellatedPatch Patch( TessFactors );
Patch.Mesh.CompactInPlace();
for( int VertexIndex : Patch.Mesh.VertexIndicesItr() )
{
FVector3d v = Patch.Mesh.GetVertex( VertexIndex );
FIntVector Barycentrics;
Barycentrics.X = FMath::RoundToInt( FMath::Clamp( v.X, 0.0f, 1.0f ) * BarycentricMax );
Barycentrics.Y = FMath::RoundToInt( FMath::Clamp( v.Y, 0.0f, 1.0f ) * BarycentricMax );
Barycentrics.Z = FMath::RoundToInt( FMath::Clamp( v.Z, 0.0f, 1.0f ) * BarycentricMax );
{
const uint32 e0 = FMath::Max3Index( Barycentrics[0], Barycentrics[1], Barycentrics[2] );
const uint32 e1 = (1 << e0) & 3;
const uint32 e2 = (1 << e1) & 3;
Barycentrics[ e0 ] = BarycentricMax - Barycentrics[ e1 ] - Barycentrics[ e2 ];
}
SnapAtEdges( Barycentrics, TessFactors );
Verts.Add( Barycentrics[0] | ( Barycentrics[1] << 16 ) );
}
for( int TriangleID : Patch.Mesh.TriangleIndicesItr() )
{
FIndex3i Triangle = Patch.Mesh.GetTriangle( TriangleID );
Indexes.Add( Triangle[0] | ( Triangle[1] << 10 ) | ( Triangle[2] << 20 ) );
}
}
#define CACHE_WINDOW_SIZE 32
// Weights for individual cache entries based on simulated annealing optimization on DemoLevel.
static int16 CacheWeightTable[ CACHE_WINDOW_SIZE ] = {
577, 616, 641, 512, 614, 635, 478, 651,
65, 213, 719, 490, 213, 726, 863, 745,
172, 939, 805, 885, 958, 1208, 1319, 1318,
1475, 1779, 2342, 159, 2307, 1998, 1211, 932
};
// Constrain index buffer to only use vertex references that are within a fixed sized trailing window from the current highest encountered vertex index.
// Triangles are reordered based on a FIFO-style cache optimization to minimize the number of vertices that need to be duplicated.
void FTessellationTable::ConstrainToCacheWindow()
{
uint32 NumOldVertices = Verts.Num();
uint32 NumOldTriangles = Indexes.Num();
check( NANITE_TESSELLATION_TABLE_SIZE <= 16 );
constexpr uint32 MaxNumTris = 16 * 16;
constexpr uint32 MaxTrianglesInDwords = ( MaxNumTris + 31 ) / 32;
uint32 VertexToTriangleMasks[ MaxNumTris * 3 ][ MaxTrianglesInDwords ] = {};
// Generate vertex to triangle masks
for( uint32 i = 0; i < NumOldTriangles; i++ )
{
const uint32 i0 = ( Indexes[i] >> 0 ) & 1023;
const uint32 i1 = ( Indexes[i] >> 10 ) & 1023;
const uint32 i2 = ( Indexes[i] >> 20 ) & 1023;
check( i0 != i1 && i1 != i2 && i2 != i0 ); // Degenerate input triangle!
check( i0 < NumOldVertices && i1 < NumOldVertices && i2 < NumOldVertices );
VertexToTriangleMasks[ i0 ][ i >> 5 ] |= 1 << ( i & 31 );
VertexToTriangleMasks[ i1 ][ i >> 5 ] |= 1 << ( i & 31 );
VertexToTriangleMasks[ i2 ][ i >> 5 ] |= 1 << ( i & 31 );
}
uint32 TrianglesEnabled[ MaxTrianglesInDwords ] = {}; // Enabled triangles are in the current material range and have not yet been visited.
uint32 TrianglesTouched[ MaxTrianglesInDwords ] = {}; // Touched triangles have had at least one of their vertices visited.
uint32 NumNewVertices = 0;
uint32 NumNewTriangles = 0;
uint16 OldToNewVertex[ MaxNumTris * 3 ];
uint32 NewVerts[ MaxNumTris * 3 ] = {}; // Initialize to make static analysis happy
uint32 NewIndexes[ MaxNumTris ];
FMemory::Memset( OldToNewVertex, -1, sizeof( OldToNewVertex ) );
uint32 DwordEnd = NumOldTriangles / 32;
uint32 BitEnd = NumOldTriangles & 31;
FMemory::Memset( TrianglesEnabled, -1, DwordEnd * sizeof( uint32 ) );
if( BitEnd != 0 )
TrianglesEnabled[ DwordEnd ] = ( 1u << BitEnd ) - 1u;
auto ScoreVertex = [ &OldToNewVertex, &NumNewVertices ] ( uint32 OldVertex )
{
uint16 NewIndex = OldToNewVertex[ OldVertex ];
int32 CacheScore = 0;
if( NewIndex != 0xFFFF )
{
uint32 CachePosition = ( NumNewVertices - 1 ) - NewIndex;
if( CachePosition < CACHE_WINDOW_SIZE )
CacheScore = CacheWeightTable[ CachePosition ];
}
return CacheScore;
};
while( true )
{
uint32 NextTriangleIndex = 0xFFFF;
int32 NextTriangleScore = 0;
// Pick highest scoring available triangle
for( uint32 TriangleDwordIndex = 0; TriangleDwordIndex < MaxTrianglesInDwords; TriangleDwordIndex++ )
{
uint32 CandidateMask = TrianglesTouched[ TriangleDwordIndex ] & TrianglesEnabled[ TriangleDwordIndex ];
while( CandidateMask )
{
uint32 TriangleDwordOffset = FMath::CountTrailingZeros( CandidateMask );
CandidateMask &= CandidateMask - 1;
int32 TriangleIndex = ( TriangleDwordIndex << 5 ) + TriangleDwordOffset;
int32 TriangleScore = 0;
TriangleScore += ScoreVertex( ( Indexes[ TriangleIndex ] >> 0 ) & 1023 );
TriangleScore += ScoreVertex( ( Indexes[ TriangleIndex ] >> 10 ) & 1023 );
TriangleScore += ScoreVertex( ( Indexes[ TriangleIndex ] >> 20 ) & 1023 );
if( TriangleScore > NextTriangleScore )
{
NextTriangleIndex = TriangleIndex;
NextTriangleScore = TriangleScore;
}
}
}
if( NextTriangleIndex == 0xFFFF )
{
// If we didn't find a triangle. It might be because it is part of a separate component. Look for an unvisited triangle to restart from.
for( uint32 TriangleDwordIndex = 0; TriangleDwordIndex < MaxTrianglesInDwords; TriangleDwordIndex++ )
{
uint32 EnableMask = TrianglesEnabled[ TriangleDwordIndex ];
if( EnableMask )
{
NextTriangleIndex = ( TriangleDwordIndex << 5 ) + FMath::CountTrailingZeros( EnableMask );
break;
}
}
if( NextTriangleIndex == 0xFFFF )
break;
}
uint32 OldIndex[3];
OldIndex[0] = ( Indexes[ NextTriangleIndex ] >> 0 ) & 1023;
OldIndex[1] = ( Indexes[ NextTriangleIndex ] >> 10 ) & 1023;
OldIndex[2] = ( Indexes[ NextTriangleIndex ] >> 20 ) & 1023;
// Mark incident triangles
for( uint32 i = 0; i < MaxTrianglesInDwords; i++ )
{
TrianglesTouched[i] |= VertexToTriangleMasks[ OldIndex[0] ][i];
TrianglesTouched[i] |= VertexToTriangleMasks[ OldIndex[1] ][i];
TrianglesTouched[i] |= VertexToTriangleMasks[ OldIndex[2] ][i];
}
uint32 NewIndex[3];
NewIndex[0] = OldToNewVertex[ OldIndex[0] ];
NewIndex[1] = OldToNewVertex[ OldIndex[1] ];
NewIndex[2] = OldToNewVertex[ OldIndex[2] ];
uint32 NumNew = (NewIndex[0] == 0xFFFF) + (NewIndex[1] == 0xFFFF) + (NewIndex[2] == 0xFFFF);
// Generate new indices such that they are all within a trailing window of CACHE_WINDOW_SIZE of NumNewVertices.
// This can require multiple iterations as new/duplicate vertices can push other vertices outside the window.
uint32 TestNumNewVertices = NumNewVertices;
TestNumNewVertices += NumNew;
while(true)
{
if (NewIndex[0] != 0xFFFF && TestNumNewVertices - NewIndex[0] >= CACHE_WINDOW_SIZE)
{
NewIndex[0] = 0xFFFF;
TestNumNewVertices++;
continue;
}
if (NewIndex[1] != 0xFFFF && TestNumNewVertices - NewIndex[1] >= CACHE_WINDOW_SIZE)
{
NewIndex[1] = 0xFFFF;
TestNumNewVertices++;
continue;
}
if (NewIndex[2] != 0xFFFF && TestNumNewVertices - NewIndex[2] >= CACHE_WINDOW_SIZE)
{
NewIndex[2] = 0xFFFF;
TestNumNewVertices++;
continue;
}
break;
}
for( int k = 0; k < 3; k++ )
{
if( NewIndex[k] == 0xFFFF)
NewIndex[k] = NumNewVertices++;
OldToNewVertex[ OldIndex[k] ] = (uint16)NewIndex[k];
NewVerts[ NewIndex[k] ] = Verts[ OldIndex[k] ];
}
// Rotate triangle such that 1st index is smallest
const uint32 i0 = FMath::Min3Index( NewIndex[0], NewIndex[1], NewIndex[2] );
const uint32 i1 = (1 << i0) & 3;
const uint32 i2 = (1 << i1) & 3;
// Output triangle
NewIndexes[ NumNewTriangles++ ] = NewIndex[ i0 ] | ( NewIndex[ i1 ] << 10 ) | ( NewIndex[ i2 ] << 20 );
// Disable selected triangle
TrianglesEnabled[ NextTriangleIndex >> 5 ] &= ~( 1 << ( NextTriangleIndex & 31 ) );
}
check( NumNewTriangles == NumOldTriangles );
if( NumNewVertices > NumOldVertices )
Verts.AddUninitialized( NumNewVertices - NumOldVertices );
check(NumNewVertices == NumOldVertices);
// Write back new triangle order
FMemory::Memcpy( Verts.GetData(), NewVerts, NumNewVertices * sizeof( uint32 ) );
FMemory::Memcpy( Indexes.GetData(), NewIndexes, NumNewTriangles * sizeof( uint32 ) );
}
void FTessellationTable::ConstrainForImmediateTessellation()
{
// Constrain such that the tessellation pattern has the same number of triangles and vertices.
// Triangles can only references vertices with an index lower or equal to the current triangle index.
// Vertex references can reference at most 32 back from the triangle index.
// Each triangle adds one new vertex and has two vertex references.
// The new vertex is always the first of the 3 indices.
const uint32 NumOldVerts = Verts.Num();
const uint32 NumOldTris = Indexes.Num();
const uint32 InvalidVert = 0xFFFFu;
TArray<uint16> OldToNewVertex;
OldToNewVertex.Init( InvalidVert, NumOldVerts );
TArray<uint32> NewVerts;
TArray<uint32> NewTris;
for (uint32 OldTriIndex = 0; OldTriIndex < NumOldTris; OldTriIndex++)
{
const uint32 IndexData = Indexes[OldTriIndex];
const uint32 Index0 = IndexData & 0x3FFu;
const uint32 Index1 = (IndexData >> 10) & 0x3FFu;
const uint32 Index2 = IndexData >> 20;
uint32 NumAddedVerts = 0;
while (true)
{
if (OldToNewVertex[Index0] == InvalidVert || NewVerts.Num() - OldToNewVertex[Index0] > 32)
{
OldToNewVertex[Index0] = NewVerts.Num();
NewVerts.Add(Verts[Index0]);
NumAddedVerts++;
continue;
}
if (OldToNewVertex[Index1] == InvalidVert || NewVerts.Num() - OldToNewVertex[Index1] > 32)
{
OldToNewVertex[Index1] = NewVerts.Num();
NewVerts.Add(Verts[Index1]);
NumAddedVerts++;
continue;
}
if (OldToNewVertex[Index2] == InvalidVert || NewVerts.Num() - OldToNewVertex[Index2] > 32)
{
OldToNewVertex[Index2] = NewVerts.Num();
NewVerts.Add(Verts[Index2]);
NumAddedVerts++;
continue;
}
if (NumAddedVerts == 0)
{
// No new vertices needed.
// Arbitrarily duplicate the Index0 vertex.
const uint32 Data = NewVerts[OldToNewVertex[Index0]];
OldToNewVertex[Index0] = NewVerts.Num();
NewVerts.Add(Data);
NumAddedVerts++;
continue;
}
break;
}
// Add any degenerate triangles
for (uint32 i = 0; i + 1 < NumAddedVerts; i++)
{
const uint32 Index = NewVerts.Num() - NumAddedVerts;
NewTris.Add( (Index << 20) | (Index << 10) | Index );
}
check(NumAddedVerts != 0);
// Add triangle
{
uint32 I0 = OldToNewVertex[Index0];
uint32 I1 = OldToNewVertex[Index1];
uint32 I2 = OldToNewVertex[Index2];
// Rotate such that first index is the highest one
while (I1 > I0 || I2 > I0)
{
uint32 Tmp = I0;
I0 = I1; I1 = I2; I2 = Tmp;
}
check(I0 == NewTris.Num());
NewTris.Add( (I2 << 20) | (I1 << 10) | I0 );
}
check(NewVerts.Num() == NewTris.Num());
}
check((uint32)NewVerts.Num() >= NumOldVerts);
check((uint32)NewTris.Num() >= NumOldTris);
if ((uint32)NewVerts.Num() > NumOldVerts)
Verts.AddUninitialized(NewVerts.Num() - NumOldVerts);
if ((uint32)NewTris.Num() > NumOldTris)
Indexes.AddUninitialized(NewTris.Num() - NumOldTris);
// Write back new triangle order
FMemory::Memcpy( Verts.GetData(), NewVerts.GetData(), NewVerts.Num() * sizeof(uint32));
FMemory::Memcpy( Indexes.GetData(), NewTris.GetData(), NewTris.Num() * sizeof(uint32));
}
void FTessellationTable::AddToVertsAndIndices( bool bImmediate, const FIntVector& TessFactors )
{
uint32 Index = (TessFactors.Z - 1u) * NANITE_TESSELLATION_TABLE_PO2_SIZE * NANITE_TESSELLATION_TABLE_PO2_SIZE +
(TessFactors.Y - 1u) * NANITE_TESSELLATION_TABLE_PO2_SIZE +
(TessFactors.X - 1u);
if (bImmediate) Index += NANITE_TESSELLATION_TABLE_PO2_SIZE * NANITE_TESSELLATION_TABLE_PO2_SIZE * NANITE_TESSELLATION_TABLE_PO2_SIZE;
const uint32 NumVerts = Verts.Num();
const uint32 NumTris = Indexes.Num();
check(NumVerts < 512);
check(NumTris < 1024);
OffsetTable[Index].X = VertsAndIndexes.Num();
OffsetTable[Index].Y = /* Pattern | */ (NumVerts << 13) | (NumTris << 22);
VertsAndIndexes.Append( Verts );
VertsAndIndexes.Append( Indexes );
Verts.Reset();
Indexes.Reset();
}
void FTessellationTable::WriteSVG( const FIntVector& TessFactors )
{
#if 0
const uint32 NumVerts = Verts.Num();
const uint32 NumTris = Indexes.Num();
char Filename[128];
sprintf(Filename, "d:\\tessellation_pattern\\%d_%d_%d.svg", TessFactors.X, TessFactors.Y, TessFactors.Z);
FILE* File = fopen(Filename, "wb");
fputs(R"xyz(<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect fill="#fff" stroke="#000" x="0" y="0" width="1024" height="1024"/>
<g opacity="0.8">
)xyz", File);
/*
Derive cartesian coordinates for patch corners using TessFactors as edge lengths.
v0 = (0,0)
v1 = (0,e0)
v2 = (x,y)
e2^2 = x^2 + y^2
e1^2 = x^2 + (e0-y)^2
e1^2 = e2^2 - y^2 + (e0-y)^2
e1^2 = e0^2 - 2*e0 * y + e2^2
y = ( e0^2 + e2^2 - e1^2 ) / ( 2*e0 )
x = sqrt( e2^2 - y^2 )
*/
FVector2f PatchCorner0( 0, 0 );
FVector2f PatchCorner1( 0, TessFactors[0] );
FVector2f PatchCorner2;
PatchCorner2.Y = float( TessFactors[0] * TessFactors[0] + TessFactors[2] * TessFactors[2] - TessFactors[1] * TessFactors[1] ) / ( 2 * TessFactors[0] );
// Limit impossible triangles which result in NaNs
if( PatchCorner2.Y < (float)TessFactors[2] )
PatchCorner2.X = FMath::Sqrt( TessFactors[2] * TessFactors[2] - PatchCorner2.Y * PatchCorner2.Y );
else
PatchCorner2.X = 1.0f;
PatchCorner0 *= 1023.0f / ( TessFactors[0] * BarycentricMax );
PatchCorner1 *= 1023.0f / ( TessFactors[0] * BarycentricMax );
PatchCorner2 *= 1023.0f / ( TessFactors[0] * BarycentricMax );
for( uint32 TriIndex = 0; TriIndex < NumTris; TriIndex++ )
{
const uint32 VertIndex0 = ( Indexes[ TriIndex ] >> 0 ) & 1023;
const uint32 VertIndex1 = ( Indexes[ TriIndex ] >> 10 ) & 1023;
const uint32 VertIndex2 = ( Indexes[ TriIndex ] >> 20 ) & 1023;
const FIntVector Barycentrics0 = GetBarycentrics( Verts[ VertIndex0 ] );
const FIntVector Barycentrics1 = GetBarycentrics( Verts[ VertIndex1 ] );
const FIntVector Barycentrics2 = GetBarycentrics( Verts[ VertIndex2 ] );
const FVector2f TriCorner0 = PatchCorner0 * Barycentrics0.X + PatchCorner1 * Barycentrics0.Y + PatchCorner2 * Barycentrics0.Z;
const FVector2f TriCorner1 = PatchCorner0 * Barycentrics1.X + PatchCorner1 * Barycentrics1.Y + PatchCorner2 * Barycentrics1.Z;
const FVector2f TriCorner2 = PatchCorner0 * Barycentrics2.X + PatchCorner1 * Barycentrics2.Y + PatchCorner2 * Barycentrics2.Z;
fprintf(File, "\t<polyline points = \"%d,%d %d,%d %d,%d %d,%d\" stroke = \"black\" stroke-width = \"4\" fill = \"none\" />\n",
int(TriCorner0.X), int(TriCorner0.Y),
int(TriCorner1.X), int(TriCorner1.Y),
int(TriCorner2.X), int(TriCorner2.Y),
int(TriCorner0.X), int(TriCorner0.Y));
}
fputs("(</g></svg>", File);
fclose(File);
#endif
}
#endif // NANITE_BUILD_TESSELLATION_TABLE
} // namespace Nanite