// Copyright 2020 Phyronnaz #include "VoxelRender/Meshers/VoxelMarchingCubeMesher.h" #include "VoxelRender/Meshers/VoxelMesherUtilities.h" #include "VoxelRender/IVoxelRenderer.h" #include "VoxelData/VoxelDataIncludes.h" #include "Transvoxel.h" #include "HAL/IConsoleManager.h" #define checkError(x) if(!(x)) { return false; } static TAutoConsoleVariable CVarEnableUniqueUVs( TEXT("voxel.mesher.UniqueUVs"), 0, TEXT("If true, will duplicate the vertices to assign to each triangle in a chunk a unique part of the UV space"), ECVF_Default); static TAutoConsoleVariable CVarRandomizeTangents( TEXT("voxel.mesher.RandomizeTangents"), 0, TEXT("If true, will randomize voxel tangents to help debug materials that should not be using them"), ECVF_Default); class FMarchingCubeHelpers { public: template static TArray CreateMesherVertices(TArray& Vertices) { VOXEL_ASYNC_FUNCTION_COUNTER(); TArray MesherVertices; MesherVertices.SetNumUninitialized(Vertices.Num()); for (int32 Index = 0; Index < Vertices.Num(); Index++) { auto& Vertex = Vertices[Index]; auto& MesherVertex = MesherVertices[Index]; MesherVertex.Position = Vertex.Position; } return MesherVertices; } template static void ComputeMaterials(TMesher& Mesher, TArray& MesherVertices, TArray& Vertices) { VOXEL_ASYNC_FUNCTION_COUNTER(); const auto GetMaterial = [&](const FIntVector& P) { return Mesher.Accelerator->GetMaterial( P.X + Mesher.ChunkPosition.X, P.Y + Mesher.ChunkPosition.Y, P.Z + Mesher.ChunkPosition.Z, Mesher.LOD); }; if (Mesher.Settings.bInterpolateColors || Mesher.Settings.bInterpolateUVs) { for (int32 Index = 0; Index < Vertices.Num(); Index++) { auto& Vertex = Vertices[Index]; auto& MesherVertex = MesherVertices[Index]; const auto PositionA = FVoxelUtilities::FloorToInt(MesherVertex.Position); const auto PositionB = FVoxelUtilities::CeilToInt(MesherVertex.Position); const auto MaterialA = GetMaterial(PositionA); const auto MaterialB = GetMaterial(PositionB); ensureVoxelSlowNoSideEffects(Vertex.MaterialPosition == PositionA || Vertex.MaterialPosition == PositionB); MesherVertex.Material = Vertex.MaterialPosition == PositionA ? MaterialA : MaterialB; const FVector Difference = MesherVertex.Position - FVector(PositionA); const float Alpha = Difference.X + Difference.Y + Difference.Z; ensureVoxelSlowNoSideEffects(0 <= Alpha && Alpha <= 1); if (Mesher.Settings.bInterpolateUVs) { if (Mesher.Settings.MaterialConfig != EVoxelMaterialConfig::MultiIndex) { MesherVertex.Material.SetU0_AsFloat(FMath::Lerp(MaterialA.GetU0_AsFloat(), MaterialB.GetU0_AsFloat(), Alpha)); MesherVertex.Material.SetV0_AsFloat(FMath::Lerp(MaterialA.GetV0_AsFloat(), MaterialB.GetV0_AsFloat(), Alpha)); MesherVertex.Material.SetU1_AsFloat(FMath::Lerp(MaterialA.GetU1_AsFloat(), MaterialB.GetU1_AsFloat(), Alpha)); MesherVertex.Material.SetV1_AsFloat(FMath::Lerp(MaterialA.GetV1_AsFloat(), MaterialB.GetV1_AsFloat(), Alpha)); } MesherVertex.Material.SetU2_AsFloat(FMath::Lerp(MaterialA.GetU2_AsFloat(), MaterialB.GetU2_AsFloat(), Alpha)); MesherVertex.Material.SetV2_AsFloat(FMath::Lerp(MaterialA.GetV2_AsFloat(), MaterialB.GetV2_AsFloat(), Alpha)); MesherVertex.Material.SetU3_AsFloat(FMath::Lerp(MaterialA.GetU3_AsFloat(), MaterialB.GetU3_AsFloat(), Alpha)); MesherVertex.Material.SetV3_AsFloat(FMath::Lerp(MaterialA.GetV3_AsFloat(), MaterialB.GetV3_AsFloat(), Alpha)); } if (Mesher.Settings.bInterpolateColors) { if (Mesher.Settings.MaterialConfig == EVoxelMaterialConfig::RGB) { MesherVertex.Material.SetColor(FMath::Lerp(MaterialA.GetLinearColor(), MaterialB.GetLinearColor(), Alpha)); } else if (Mesher.Settings.MaterialConfig == EVoxelMaterialConfig::SingleIndex) { MesherVertex.Material.SetR_AsFloat(FMath::Lerp(MaterialA.GetR_AsFloat(), MaterialB.GetR_AsFloat(), Alpha)); MesherVertex.Material.SetG_AsFloat(FMath::Lerp(MaterialA.GetG_AsFloat(), MaterialB.GetG_AsFloat(), Alpha)); MesherVertex.Material.SetB_AsFloat(FMath::Lerp(MaterialA.GetB_AsFloat(), MaterialB.GetB_AsFloat(), Alpha)); } else { checkVoxelSlow(Mesher.Settings.MaterialConfig == EVoxelMaterialConfig::MultiIndex); MesherVertex.Material.SetMultiIndex_Blend0_AsFloat(FMath::Lerp(MaterialA.GetMultiIndex_Blend0_AsFloat(), MaterialB.GetMultiIndex_Blend0_AsFloat(), Alpha)); MesherVertex.Material.SetMultiIndex_Blend1_AsFloat(FMath::Lerp(MaterialA.GetMultiIndex_Blend1_AsFloat(), MaterialB.GetMultiIndex_Blend1_AsFloat(), Alpha)); MesherVertex.Material.SetMultiIndex_Blend2_AsFloat(FMath::Lerp(MaterialA.GetMultiIndex_Blend2_AsFloat(), MaterialB.GetMultiIndex_Blend2_AsFloat(), Alpha)); MesherVertex.Material.SetMultiIndex_Wetness_AsFloat(FMath::Lerp(MaterialA.GetMultiIndex_Wetness_AsFloat(), MaterialB.GetMultiIndex_Wetness_AsFloat(), Alpha)); } } } } else { for (int32 Index = 0; Index < Vertices.Num(); Index++) { auto& Vertex = Vertices[Index]; auto& MesherVertex = MesherVertices[Index]; MesherVertex.Material = GetMaterial(Vertex.MaterialPosition); } } } static void FixupTangents(TArray& MesherVertices) { if (CVarRandomizeTangents.GetValueOnAnyThread() != 0) { for (auto& Vertex : MesherVertices) { Vertex.Tangent.TangentX = FVector(FMath::FRandRange(-1., 1.), FMath::FRandRange(-1., 1.), FMath::FRandRange(-1., 1.)).GetSafeNormal(); } } } static void ComputeFlatNormals(TArray& Vertices, TArray& Indices) { VOXEL_ASYNC_FUNCTION_COUNTER(); TArray NewVertices; for (int32 I = 0; I < Indices.Num(); I += 3) { uint32& IndexA = Indices[I + 0]; uint32& IndexB = Indices[I + 1]; uint32& IndexC = Indices[I + 2]; auto VertexA = Vertices[IndexA]; auto VertexB = Vertices[IndexB]; auto VertexC = Vertices[IndexC]; const FVector Normal = FVector::CrossProduct(VertexC.Position - VertexA.Position, VertexB.Position - VertexA.Position).GetSafeNormal(); VertexA.Normal = Normal; VertexB.Normal = Normal; VertexC.Normal = Normal; IndexA = NewVertices.Add(VertexA); IndexB = NewVertices.Add(VertexB); IndexC = NewVertices.Add(VertexC); } Vertices = MoveTemp(NewVertices); } static void ComputeNormals(FVoxelMarchingCubeMesher& Mesher, TArray& MesherVertices, TArray& Indices) { VOXEL_ASYNC_FUNCTION_COUNTER(); const auto GetGradient = [&](const FVector& Position) { if (Mesher.LOD == 0) { // For LOD 0, we used the cached data // One downside: might not look as great as using the float value from the generator // However, the generator at LOD 0 should have small enough values to generate nice normals from them even if they are FVoxelValues // Additionally, computing the normals from the generator often requires the interpolation fix at LOD 0, // which ends up doing the same as what we are doing here but a lot slower (eg 50ms of 54ms total taken to compute normals!) return FVoxelDataUtilities::GetGradientFromGetValue( FVoxelDataUtilities::MakeBilinearInterpolatedData(Mesher), Position.X, Position.Y, Position.Z, 0, 1); } else { return FVoxelDataUtilities::GetGradientFromGetFloatValue( *Mesher.Accelerator, v_flt(Position.X) + Mesher.ChunkPosition.X, v_flt(Position.Y) + Mesher.ChunkPosition.Y, v_flt(Position.Z) + Mesher.ChunkPosition.Z, Mesher.LOD, Mesher.Step); } }; if (Mesher.Settings.NormalConfig == EVoxelNormalConfig::GradientNormal) { for (auto& Vertex : MesherVertices) { Vertex.Normal = GetGradient(Vertex.Position); Vertex.Tangent = FVoxelProcMeshTangent(); } } else if (Mesher.Settings.NormalConfig == EVoxelNormalConfig::FlatNormal) { ComputeFlatNormals(MesherVertices, Indices); } else if (Mesher.Settings.NormalConfig == EVoxelNormalConfig::MeshNormal) { for (auto& Vertex : MesherVertices) { Vertex.Normal = FVector(0, 0, 0); Vertex.Tangent = FVoxelProcMeshTangent(); } for (int32 Index = 0; Index < Indices.Num(); Index += 3) { auto& A = MesherVertices[Indices[Index + 0]]; auto& B = MesherVertices[Indices[Index + 1]]; auto& C = MesherVertices[Indices[Index + 2]]; const FVector Normal = FVector::CrossProduct(C.Position - A.Position, B.Position - A.Position).GetSafeNormal(); A.Normal += Normal; B.Normal += Normal; C.Normal += Normal; } for (auto& Vertex : MesherVertices) { if (Vertex.Position.X < Mesher.Step || Vertex.Position.Y < Mesher.Step || Vertex.Position.Z < Mesher.Step || Vertex.Position.X > (RENDER_CHUNK_SIZE - 1) * Mesher.Step || Vertex.Position.Y > (RENDER_CHUNK_SIZE - 1) * Mesher.Step || Vertex.Position.Z > (RENDER_CHUNK_SIZE - 1) * Mesher.Step) { // Can't use mesh normals on edges, as it looks like crap because of the missing neighbor vertices Vertex.Normal = GetGradient(Vertex.Position); } else { Vertex.Normal.Normalize(); } } } else { check(Mesher.Settings.NormalConfig == EVoxelNormalConfig::NoNormal); for (auto& Vertex : MesherVertices) { Vertex.Normal = FVector(0, 0, 0); Vertex.Tangent = FVoxelProcMeshTangent(); } } FixupTangents(MesherVertices); } static void ComputeNormals(FVoxelMarchingCubeTransitionsMesher& Mesher, TArray& MesherVertices, TArray& Indices) { VOXEL_ASYNC_FUNCTION_COUNTER(); if (Mesher.Settings.NormalConfig == EVoxelNormalConfig::GradientNormal || Mesher.Settings.NormalConfig == EVoxelNormalConfig::MeshNormal) { for (auto& Vertex : MesherVertices) { Vertex.Normal = FVoxelDataUtilities::GetGradientFromGetFloatValue( *Mesher.Accelerator, v_flt(Vertex.Position.X) + Mesher.ChunkPosition.X, v_flt(Vertex.Position.Y) + Mesher.ChunkPosition.Y, v_flt(Vertex.Position.Z) + Mesher.ChunkPosition.Z, Mesher.LOD, Mesher.Step);; Vertex.Tangent = FVoxelProcMeshTangent(); } } else if (Mesher.Settings.NormalConfig == EVoxelNormalConfig::FlatNormal) { ComputeFlatNormals(MesherVertices, Indices); } else { check(Mesher.Settings.NormalConfig == EVoxelNormalConfig::NoNormal); for (auto& Vertex : MesherVertices) { Vertex.Normal = FVector(0, 0, 0); Vertex.Tangent = FVoxelProcMeshTangent(); } } FixupTangents(MesherVertices); } template static void ComputeUVs(TMesher& Mesher, TArray& MesherVertices) { VOXEL_ASYNC_FUNCTION_COUNTER(); for (auto& Vertex : MesherVertices) { Vertex.TextureCoordinate = FVoxelMesherUtilities::GetUVs(Mesher, Vertex.Position); } } }; FVoxelIntBox FVoxelMarchingCubeMesher::GetBoundsToCheckIsEmptyOn() const { return FVoxelIntBox(ChunkPosition, ChunkPosition + CHUNK_SIZE_WITH_END_EDGE * Step); } FVoxelIntBox FVoxelMarchingCubeMesher::GetBoundsToLock() const { // We need to lock for the normals (also work with LOD 0) return FVoxelIntBox(ChunkPosition - FIntVector(Step), ChunkPosition + FIntVector(Step) + CHUNK_SIZE_WITH_END_EDGE * Step); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// TVoxelSharedPtr FVoxelMarchingCubeMesher::CreateFullChunkImpl(FVoxelMesherTimes& Times) { VOXEL_ASYNC_FUNCTION_COUNTER(); struct FLocalVertex { FVector Position; FIntVector MaterialPosition; FLocalVertex() = default; FORCEINLINE FLocalVertex(const FVector& Position, const FIntVector& MaterialPosition) : Position(Position) , MaterialPosition(MaterialPosition) { } }; TArray Indices; TArray Vertices; CreateGeometryTemplate(Times, Indices, Vertices); FVoxelMesherUtilities::SanitizeMesh(Indices, Vertices); TArray MesherVertices = FMarchingCubeHelpers::CreateMesherVertices(Vertices); MESHER_TIME_MATERIALS(MesherVertices.Num(), FMarchingCubeHelpers::ComputeMaterials(*this, MesherVertices, Vertices)); MESHER_TIME(Normals, FMarchingCubeHelpers::ComputeNormals(*this, MesherVertices, Indices)); UnlockData(); MESHER_TIME(UVs, FMarchingCubeHelpers::ComputeUVs(*this, MesherVertices)); if (CVarEnableUniqueUVs.GetValueOnAnyThread() != 0) { { TArray NewVertices; NewVertices.Reserve(Indices.Num()); TArray NewIndices; NewIndices.Reserve(Indices.Num()); for (uint32 Index : Indices) { NewIndices.Add(NewVertices.Num()); NewVertices.Add(MesherVertices[Index]); } MesherVertices = MoveTemp(NewVertices); Indices = MoveTemp(NewIndices); } const int32 NumTriangles = Indices.Num() / 3; const int32 NumSquares = FVoxelUtilities::DivideCeil(NumTriangles, 2); const int32 NumSquaresPerRow = FMath::CeilToInt(FMath::Sqrt(double(NumSquares))); check(NumSquares <= NumSquaresPerRow * NumSquaresPerRow); const int32 TextureUVSize = 1; const float SquareUVSize = TextureUVSize / float(NumSquaresPerRow); const auto AddTriangle = [&Indices, &MesherVertices](int32 TriangleIndex, const FVector2D& A, const FVector2D& B, const FVector2D& C) { const int32 IndexA = Indices[3 * TriangleIndex + 0]; const int32 IndexB = Indices[3 * TriangleIndex + 1]; const int32 IndexC = Indices[3 * TriangleIndex + 2]; MesherVertices[IndexA].TextureCoordinate = A; MesherVertices[IndexB].TextureCoordinate = B; MesherVertices[IndexC].TextureCoordinate = C; }; int32 Row = 0; int32 Column = 0; for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; TriangleIndex += 2) { /** * A B * C D */ const FVector2D A = { (Column + 0) * SquareUVSize, (Row + 0) * SquareUVSize }; const FVector2D B = { (Column + 1) * SquareUVSize, (Row + 0) * SquareUVSize }; const FVector2D C = { (Column + 0) * SquareUVSize, (Row + 1) * SquareUVSize }; const FVector2D D = { (Column + 1) * SquareUVSize, (Row + 1) * SquareUVSize }; AddTriangle(TriangleIndex, A, B, D); if (TriangleIndex + 1 < NumTriangles) { AddTriangle(TriangleIndex + 1, A, D, C); } Column++; if (Column == NumSquaresPerRow) { Column = 0; Row++; } } } return MESHER_TIME_RETURN(CreateChunk, FVoxelMesherUtilities::CreateChunkFromVertices( Settings, LOD, MoveTemp(Indices), MoveTemp(MesherVertices))); } void FVoxelMarchingCubeMesher::CreateGeometryImpl(FVoxelMesherTimes& Times, TArray& Indices, TArray& Vertices) { VOXEL_ASYNC_FUNCTION_COUNTER(); struct FVectorVertex : FVector { FVectorVertex() = default; FORCEINLINE FVectorVertex(const FVector& Position, const FIntVector&) : FVector(Position) { } }; CreateGeometryTemplate(Times, Indices, reinterpret_cast&>(Vertices)); UnlockData(); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// template bool FVoxelMarchingCubeMesher::CreateGeometryTemplate(FVoxelMesherTimes& Times, TArray& Indices, TArray& Vertices) { VOXEL_ASYNC_FUNCTION_COUNTER(); const int32 DataSize = LOD == 0 ? CHUNK_SIZE_WITH_NORMALS : CHUNK_SIZE_WITH_END_EDGE; FVoxelIntBox BoundsToQuery(ChunkPosition, ChunkPosition + CHUNK_SIZE_WITH_END_EDGE * Step); if (LOD == 0) { // Account for normals BoundsToQuery = BoundsToQuery.Extend(1); } TVoxelQueryZone QueryZone(BoundsToQuery, FIntVector(DataSize), LOD, CachedValues); MESHER_TIME_VALUES(DataSize * DataSize * DataSize, Data.Get(QueryZone, LOD)); Accelerator = MakeUnique(Data, GetBoundsToLock()); uint32 VoxelIndex = 0; if (LOD == 0) VoxelIndex += DataSize * DataSize; // Additional voxel for normals for (int32 LZ = 0; LZ < RENDER_CHUNK_SIZE; LZ++) { if (LOD == 0) VoxelIndex += DataSize; // Additional voxel for normals for (int32 LY = 0; LY < RENDER_CHUNK_SIZE; LY++) { if (LOD == 0) VoxelIndex += 1; // Additional voxel for normals for (int32 LX = 0; LX < RENDER_CHUNK_SIZE; LX++) { { CurrentCache[GetCacheIndex(0, LX, LY)] = -1; // Set EdgeIndex 0 to -1 if the cell isn't voxelized, eg all corners = 0 uint32 CubeIndices[8]; CubeIndices[0] = VoxelIndex; CubeIndices[1] = VoxelIndex + 1; CubeIndices[2] = VoxelIndex + DataSize; CubeIndices[3] = VoxelIndex + 1 + DataSize; CubeIndices[4] = VoxelIndex + DataSize * DataSize; CubeIndices[5] = VoxelIndex + 1 + DataSize * DataSize; CubeIndices[6] = VoxelIndex + DataSize + DataSize * DataSize; CubeIndices[7] = VoxelIndex + 1 + DataSize + DataSize * DataSize; checkVoxelSlow(CubeIndices[0] < uint32(DataSize * DataSize * DataSize)); checkVoxelSlow(CubeIndices[1] < uint32(DataSize * DataSize * DataSize)); checkVoxelSlow(CubeIndices[2] < uint32(DataSize * DataSize * DataSize)); checkVoxelSlow(CubeIndices[3] < uint32(DataSize * DataSize * DataSize)); checkVoxelSlow(CubeIndices[4] < uint32(DataSize * DataSize * DataSize)); checkVoxelSlow(CubeIndices[5] < uint32(DataSize * DataSize * DataSize)); checkVoxelSlow(CubeIndices[6] < uint32(DataSize * DataSize * DataSize)); checkVoxelSlow(CubeIndices[7] < uint32(DataSize * DataSize * DataSize)); const uint32 CaseCode = (CachedValues[CubeIndices[0]].IsEmpty() << 0) | (CachedValues[CubeIndices[1]].IsEmpty() << 1) | (CachedValues[CubeIndices[2]].IsEmpty() << 2) | (CachedValues[CubeIndices[3]].IsEmpty() << 3) | (CachedValues[CubeIndices[4]].IsEmpty() << 4) | (CachedValues[CubeIndices[5]].IsEmpty() << 5) | (CachedValues[CubeIndices[6]].IsEmpty() << 6) | (CachedValues[CubeIndices[7]].IsEmpty() << 7); if (CaseCode != 0 && CaseCode != 255) { // Cell has a nontrivial triangulation const uint8 ValidityMask = (LX != 0) + 2 * (LY != 0) + 4 * (LZ != 0); checkVoxelSlow(0 <= CaseCode && CaseCode < 256); const uint8 CellClass = Transvoxel::regularCellClass[CaseCode]; const uint16* RESTRICT VertexData = Transvoxel::regularVertexData[CaseCode]; checkVoxelSlow(0 <= CellClass && CellClass < 16); Transvoxel::RegularCellData CellData = Transvoxel::regularCellData[CellClass]; // Indices of the vertices used in this cube TVoxelStaticArray VertexIndices; for (int32 I = 0; I < CellData.GetVertexCount(); I++) { int32 VertexIndex = -2; const uint16 EdgeCode = VertexData[I]; // A: low point / B: high point const uint8 LocalIndexA = (EdgeCode >> 4) & 0x0F; const uint8 LocalIndexB = EdgeCode & 0x0F; checkVoxelSlow(0 <= LocalIndexA && LocalIndexA < 8); checkVoxelSlow(0 <= LocalIndexB && LocalIndexB < 8); const uint32 IndexA = CubeIndices[LocalIndexA]; const uint32 IndexB = CubeIndices[LocalIndexB]; const FVoxelValue& ValueAtA = CachedValues[IndexA]; const FVoxelValue& ValueAtB = CachedValues[IndexB]; checkVoxelSlow(ValueAtA.IsEmpty() != ValueAtB.IsEmpty()); uint8 EdgeIndex = ((EdgeCode >> 8) & 0x0F); checkVoxelSlow(1 <= EdgeIndex && EdgeIndex < 4); // Direction to go to use an already created vertex: // first bit: x is different // second bit: y is different // third bit: z is different // fourth bit: vertex isn't cached uint8 CacheDirection = EdgeCode >> 12; if (ValueAtA.IsNull()) { EdgeIndex = 0; CacheDirection = LocalIndexA ^ 7; } if (ValueAtB.IsNull()) { checkVoxelSlow(!ValueAtA.IsNull()); EdgeIndex = 0; CacheDirection = LocalIndexB ^ 7; } const bool bIsVertexCached = ((ValidityMask & CacheDirection) == CacheDirection) && CacheDirection; // CacheDirection == 0 => LocalIndexB = 0 (as only B can be = 7) and ValueAtB = 0 if (bIsVertexCached) { checkVoxelSlow(!(CacheDirection & 0x08)); bool XIsDifferent = !!(CacheDirection & 0x01); bool YIsDifferent = !!(CacheDirection & 0x02); bool ZIsDifferent = !!(CacheDirection & 0x04); VertexIndex = (ZIsDifferent ? OldCache : CurrentCache)[GetCacheIndex(EdgeIndex, LX - XIsDifferent, LY - YIsDifferent)]; ensureVoxelSlowNoSideEffects(-1 <= VertexIndex && VertexIndex < Vertices.Num()); // Can happen if the generator is returning different values } if (!bIsVertexCached || VertexIndex == -1) { // We are on one the lower edges of the chunk. Compute vertex const FIntVector PositionA((LX + (LocalIndexA & 0x01)) * Step, (LY + ((LocalIndexA & 0x02) >> 1)) * Step, (LZ + ((LocalIndexA & 0x04) >> 2)) * Step); const FIntVector PositionB((LX + (LocalIndexB & 0x01)) * Step, (LY + ((LocalIndexB & 0x02) >> 1)) * Step, (LZ + ((LocalIndexB & 0x04) >> 2)) * Step); FVector IntersectionPoint; FIntVector MaterialPosition; if (EdgeIndex == 0) { if (ValueAtA.IsNull()) { IntersectionPoint = FVector(PositionA); MaterialPosition = PositionA; } else { checkVoxelSlow(ValueAtB.IsNull()); IntersectionPoint = FVector(PositionB); MaterialPosition = PositionB; } } else if (LOD == 0) { // Full resolution const float Alpha = ValueAtA.ToFloat() / (ValueAtA.ToFloat() - ValueAtB.ToFloat()); checkError(!FMath::IsNaN(Alpha) && FMath::IsFinite(Alpha)); switch (EdgeIndex) { case 2: // X IntersectionPoint = FVector(FMath::Lerp(PositionA.X, PositionB.X, Alpha), PositionA.Y, PositionA.Z); break; case 1: // Y IntersectionPoint = FVector(PositionA.X, FMath::Lerp(PositionA.Y, PositionB.Y, Alpha), PositionA.Z); break; case 3: // Z IntersectionPoint = FVector(PositionA.X, PositionA.Y, FMath::Lerp(PositionA.Z, PositionB.Z, Alpha)); break; default: checkVoxelSlow(false); } // Use the material of the point inside MaterialPosition = !ValueAtA.IsEmpty() ? PositionA : PositionB; } else { // Interpolate const bool bIsAlongX = (EdgeIndex == 2); const bool bIsAlongY = (EdgeIndex == 1); const bool bIsAlongZ = (EdgeIndex == 3); checkVoxelSlow(!bIsAlongX || (PositionA.Y == PositionB.Y && PositionA.Z == PositionB.Z)); checkVoxelSlow(!bIsAlongY || (PositionA.X == PositionB.X && PositionA.Z == PositionB.Z)); checkVoxelSlow(!bIsAlongZ || (PositionA.X == PositionB.X && PositionA.Y == PositionB.Y)); int32 Min = bIsAlongX ? PositionA.X : bIsAlongY ? PositionA.Y : PositionA.Z; int32 Max = bIsAlongX ? PositionB.X : bIsAlongY ? PositionB.Y : PositionB.Z; FVoxelValue ValueAtACopy = ValueAtA; FVoxelValue ValueAtBCopy = ValueAtB; while (Max - Min != 1) { checkError((Max + Min) % 2 == 0); const int32 Middle = (Max + Min) / 2; FVoxelValue ValueAtMiddle = MESHER_TIME_RETURN_VALUES(1, Accelerator->Get( (bIsAlongX ? Middle : PositionA.X) + ChunkPosition.X, (bIsAlongY ? Middle : PositionA.Y) + ChunkPosition.Y, (bIsAlongZ ? Middle : PositionA.Z) + ChunkPosition.Z, LOD)); if (ValueAtACopy.IsEmpty() == ValueAtMiddle.IsEmpty()) { // If min and middle have same sign Min = Middle; ValueAtACopy = ValueAtMiddle; } else { // If max and middle have same sign Max = Middle; ValueAtBCopy = ValueAtMiddle; } checkError(Min <= Max); } const float Alpha = ValueAtACopy.ToFloat() / (ValueAtACopy.ToFloat() - ValueAtBCopy.ToFloat()); checkError(!FMath::IsNaN(Alpha) && FMath::IsFinite(Alpha)); const float R = FMath::Lerp(Min, Max, Alpha); IntersectionPoint = FVector( bIsAlongX ? R : PositionA.X, bIsAlongY ? R : PositionA.Y, bIsAlongZ ? R : PositionA.Z); // Get intersection material if (!ValueAtACopy.IsEmpty()) { checkVoxelSlow(ValueAtBCopy.IsEmpty()); MaterialPosition = FIntVector( bIsAlongX ? Min : PositionA.X, bIsAlongY ? Min : PositionA.Y, bIsAlongZ ? Min : PositionA.Z); } else { checkVoxelSlow(!ValueAtBCopy.IsEmpty()); MaterialPosition = FIntVector( bIsAlongX ? Max : PositionA.X, bIsAlongY ? Max : PositionA.Y, bIsAlongZ ? Max : PositionA.Z); } } VertexIndex = Vertices.Num(); if (Settings.RenderSharpness != 0) { IntersectionPoint = FVector(FVoxelUtilities::RoundToInt(IntersectionPoint * Settings.RenderSharpness)) / Settings.RenderSharpness; } Vertices.Add(T(IntersectionPoint, MaterialPosition)); checkVoxelSlow((ValueAtB.IsNull() && LocalIndexB == 7) == !CacheDirection); checkVoxelSlow(CacheDirection || EdgeIndex == 0); // Save vertex if not on edge if (CacheDirection & 0x08 || !CacheDirection) // ValueAtB.IsNull() && LocalIndexB == 7 => !CacheDirection { CurrentCache[GetCacheIndex(EdgeIndex, LX, LY)] = VertexIndex; } } VertexIndices[I] = VertexIndex; checkVoxelSlow(0 <= VertexIndex && VertexIndex < Vertices.Num()); } // Add triangles // 3 vertex per triangle for (int32 Index = 0; Index < 3 * CellData.GetTriangleCount(); Index += 3) { Indices.Add(VertexIndices[CellData.vertexIndex[Index + 0]]); Indices.Add(VertexIndices[CellData.vertexIndex[Index + 1]]); Indices.Add(VertexIndices[CellData.vertexIndex[Index + 2]]); } } } VoxelIndex++; } VoxelIndex += 1; // End edge voxel if (LOD == 0) VoxelIndex += 1; // Additional voxel for normals } VoxelIndex += DataSize; // End edge voxel if (LOD == 0) VoxelIndex += DataSize; // Additional voxel for normals // Can't use Unreal Swap on restrict ptrs with clang std::swap(CurrentCache, OldCache); } return true; } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FORCEINLINE int32 FVoxelMarchingCubeMesher::GetCacheIndex(int32 EdgeIndex, int32 LX, int32 LY) { checkVoxelSlow(0 <= LX && LX < RENDER_CHUNK_SIZE); checkVoxelSlow(0 <= LY && LY < RENDER_CHUNK_SIZE); checkVoxelSlow(0 <= EdgeIndex && EdgeIndex < EDGE_INDEX_COUNT); return EdgeIndex + LX * EDGE_INDEX_COUNT + LY * EDGE_INDEX_COUNT * RENDER_CHUNK_SIZE; } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FVoxelIntBox FVoxelMarchingCubeTransitionsMesher::GetBoundsToCheckIsEmptyOn() const { return FVoxelIntBox(ChunkPosition, ChunkPosition + CHUNK_SIZE_WITH_END_EDGE * Step); } FVoxelIntBox FVoxelMarchingCubeTransitionsMesher::GetBoundsToLock() const { return FVoxelIntBox(ChunkPosition - FIntVector(Step), ChunkPosition + FIntVector(Step) + CHUNK_SIZE_WITH_END_EDGE * Step); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// template bool FVoxelMarchingCubeTransitionsMesher::CreateGeometryTemplate(FVoxelMesherTimes& Times, TArray& Indices, TArray& Vertices) { VOXEL_ASYNC_FUNCTION_COUNTER(); Accelerator = MakeUnique(Data, GetBoundsToLock()); bool bSuccess = true; bSuccess &= CreateGeometryForDirection(Times, Indices, Vertices); bSuccess &= CreateGeometryForDirection(Times, Indices, Vertices); bSuccess &= CreateGeometryForDirection(Times, Indices, Vertices); bSuccess &= CreateGeometryForDirection(Times, Indices, Vertices); bSuccess &= CreateGeometryForDirection(Times, Indices, Vertices); bSuccess &= CreateGeometryForDirection(Times, Indices, Vertices); return bSuccess; } template bool FVoxelMarchingCubeTransitionsMesher::CreateGeometryForDirection(FVoxelMesherTimes& Times, TArray& Indices, TArray& Vertices) { if (!(TransitionsMask & Direction)) return true; #if VOXEL_DEBUG for (auto& Value : Cache2D) { Value = -100; } #endif for (int32 LX = 0; LX < RENDER_CHUNK_SIZE; LX++) { for (int32 LY = 0; LY < RENDER_CHUNK_SIZE; LY++) { // Set EdgeIndex 0, 1, 2 and 7 to -1 for when the cell aren't polygonized (0 on all corners) Cache2D[GetCacheIndex(0, LX, LY)] = -1; Cache2D[GetCacheIndex(1, LX, LY)] = -1; Cache2D[GetCacheIndex(2, LX, LY)] = -1; Cache2D[GetCacheIndex(7, LX, LY)] = -1; FVoxelValue CornerValues[13]; { MESHER_TIME_SCOPE_VALUES(13); CornerValues[0] = GetValue((2 * LX + 0) * HalfStep, (2 * LY + 0) * HalfStep, HalfLOD); CornerValues[1] = GetValue((2 * LX + 1) * HalfStep, (2 * LY + 0) * HalfStep, HalfLOD); CornerValues[2] = GetValue((2 * LX + 2) * HalfStep, (2 * LY + 0) * HalfStep, HalfLOD); CornerValues[3] = GetValue((2 * LX + 0) * HalfStep, (2 * LY + 1) * HalfStep, HalfLOD); CornerValues[4] = GetValue((2 * LX + 1) * HalfStep, (2 * LY + 1) * HalfStep, HalfLOD); CornerValues[5] = GetValue((2 * LX + 2) * HalfStep, (2 * LY + 1) * HalfStep, HalfLOD); CornerValues[6] = GetValue((2 * LX + 0) * HalfStep, (2 * LY + 2) * HalfStep, HalfLOD); CornerValues[7] = GetValue((2 * LX + 1) * HalfStep, (2 * LY + 2) * HalfStep, HalfLOD); CornerValues[8] = GetValue((2 * LX + 2) * HalfStep, (2 * LY + 2) * HalfStep, HalfLOD); CornerValues[9] = GetValue((LX + 0) * Step, (LY + 0) * Step, LOD); CornerValues[10] = GetValue((LX + 1) * Step, (LY + 0) * Step, LOD); CornerValues[11] = GetValue((LX + 0) * Step, (LY + 1) * Step, LOD); CornerValues[12] = GetValue((LX + 1) * Step, (LY + 1) * Step, LOD); } if (CornerValues[9].IsEmpty() != CornerValues[0].IsEmpty()) { CornerValues[9] = CornerValues[0]; } if (CornerValues[10].IsEmpty() != CornerValues[2].IsEmpty()) { CornerValues[10] = CornerValues[2]; } if (CornerValues[11].IsEmpty() != CornerValues[6].IsEmpty()) { CornerValues[11] = CornerValues[6]; } if (CornerValues[12].IsEmpty() != CornerValues[8].IsEmpty()) { CornerValues[12] = CornerValues[8]; } const uint32 CaseCode = (CornerValues[0].IsEmpty() << 0) | (CornerValues[1].IsEmpty() << 1) | (CornerValues[2].IsEmpty() << 2) | (CornerValues[5].IsEmpty() << 3) | (CornerValues[8].IsEmpty() << 4) | (CornerValues[7].IsEmpty() << 5) | (CornerValues[6].IsEmpty() << 6) | (CornerValues[3].IsEmpty() << 7) | (CornerValues[4].IsEmpty() << 8); if (!(CaseCode == 0 || CaseCode == 511)) { const uint8 ValidityMask = (LX != 0) + 2 * (LY != 0); FIntVector Positions[13] = { FIntVector(2 * LX + 0, 2 * LY + 0, 0) * HalfStep, FIntVector(2 * LX + 1, 2 * LY + 0, 0) * HalfStep, FIntVector(2 * LX + 2, 2 * LY + 0, 0) * HalfStep, FIntVector(2 * LX + 0, 2 * LY + 1, 0) * HalfStep, FIntVector(2 * LX + 1, 2 * LY + 1, 0) * HalfStep, FIntVector(2 * LX + 2, 2 * LY + 1, 0) * HalfStep, FIntVector(2 * LX + 0, 2 * LY + 2, 0) * HalfStep, FIntVector(2 * LX + 1, 2 * LY + 2, 0) * HalfStep, FIntVector(2 * LX + 2, 2 * LY + 2, 0) * HalfStep, FIntVector(2 * LX + 0, 2 * LY + 0, 1) * HalfStep, FIntVector(2 * LX + 2, 2 * LY + 0, 1) * HalfStep, FIntVector(2 * LX + 0, 2 * LY + 2, 1) * HalfStep, FIntVector(2 * LX + 2, 2 * LY + 2, 1) * HalfStep }; checkVoxelSlow(0 <= CaseCode && CaseCode < 512); const uint8 CellClass = Transvoxel::transitionCellClass[CaseCode]; const uint16* VertexData = Transvoxel::transitionVertexData[CaseCode]; checkVoxelSlow(0 <= (CellClass & 0x7F) && (CellClass & 0x7F) < 56); const Transvoxel::TransitionCellData CellData = Transvoxel::transitionCellData[CellClass & 0x7F]; const bool bFlip = ((CellClass >> 7) != 0); TArray> VertexIndices; // Not sure how many indices max, let's just say 64 VertexIndices.SetNumUninitialized(CellData.GetVertexCount()); for (int32 i = 0; i < CellData.GetVertexCount(); i++) { int32 VertexIndex = -1; const uint16& EdgeCode = VertexData[i]; // A: low point / B: high point const uint8 IndexVertexA = (EdgeCode >> 4) & 0x0F; const uint8 IndexVertexB = EdgeCode & 0x0F; checkVoxelSlow(0 <= IndexVertexA && IndexVertexA < 13); checkVoxelSlow(0 <= IndexVertexB && IndexVertexB < 13); const FIntVector& PositionA = Positions[IndexVertexA]; const FIntVector& PositionB = Positions[IndexVertexB]; const FVoxelValue& ValueAtA = CornerValues[IndexVertexA]; const FVoxelValue& ValueAtB = CornerValues[IndexVertexB]; uint8 EdgeIndex = (EdgeCode >> 8) & 0x0F; checkVoxelSlow(EdgeIndex < 10); // Direction to go to use an already created vertex // First bit: x is different // Second bit: y is different // Third bit: interior edge, never cached // Fourth bit: own edge, need to create uint8 CacheDirection = EdgeCode >> 12; if (!(CacheDirection & 0x04)) // If not interior edge { static uint8 CacheDirectionMap[13] = {3, 2, 2, 1, 4, 8, 1, 8, 8, 3, 2, 1, 8}; if (ValueAtA.IsNull()) { static uint8 EdgeIndexMap[10] = {0, 1, 2, 0, 1, 0, 2, 7, 7, 7}; EdgeIndex = EdgeIndexMap[EdgeIndex]; CacheDirection = CacheDirectionMap[IndexVertexA]; } if (ValueAtB.IsNull()) { checkVoxelSlow(!ValueAtA.IsNull()); static uint8 EdgeIndexMap[10] = {0, 1, 2, 1, 0, 2, 0, 7, 7, 7}; EdgeIndex = EdgeIndexMap[EdgeIndex]; CacheDirection = CacheDirectionMap[IndexVertexB]; } } const bool bIsVertexCached = ((ValidityMask & CacheDirection) == CacheDirection); if (bIsVertexCached) { checkVoxelSlow(!(CacheDirection & 0x08) && !(CacheDirection & 0x04)); const bool XIsDifferent = !!(CacheDirection & 0x01); const bool YIsDifferent = !!(CacheDirection & 0x02); VertexIndex = Cache2D[GetCacheIndex(EdgeIndex, LX - XIsDifferent, LY - YIsDifferent)]; checkVoxelSlow(-1 <= VertexIndex && VertexIndex < Vertices.Num()); } if (!bIsVertexCached || VertexIndex == -1) { FVector IntersectionPoint; FIntVector MaterialPosition; const bool bIsLowResChunk = EdgeIndex == 7 || EdgeIndex == 8 || EdgeIndex == 9; if (EdgeIndex == 0 || EdgeIndex == 1 || EdgeIndex == 2 || EdgeIndex == 7) { if (ValueAtA.IsNull()) { const auto P = Local2DToGlobal(PositionA.X, PositionA.Y, 0); IntersectionPoint = FVector(P); MaterialPosition = P; } else { checkVoxelSlow(ValueAtB.IsNull()); const auto P = Local2DToGlobal(PositionB.X, PositionB.Y, 0); IntersectionPoint = FVector(P); MaterialPosition = P; } } else { const bool bIsAlongX = EdgeIndex == 3 || EdgeIndex == 4 || EdgeIndex == 8; const bool bIsAlongY = EdgeIndex == 5 || EdgeIndex == 6 || EdgeIndex == 9; checkVoxelSlow((bIsAlongX && !bIsAlongY) || (!bIsAlongX && bIsAlongY)); int32 Min = bIsAlongX ? PositionA.X : PositionA.Y; int32 Max = bIsAlongX ? PositionB.X : PositionB.Y; FVoxelValue ValueAtACopy = ValueAtA; FVoxelValue ValueAtBCopy = ValueAtB; while (Max - Min != 1) { checkError((Max + Min) % 2 == 0); const int32 Middle = (Max + Min) / 2; FVoxelValue ValueAtMiddle = MESHER_TIME_RETURN_VALUES(1, GetValue( bIsAlongX ? Middle : PositionA.X, bIsAlongY ? Middle : PositionA.Y, bIsLowResChunk ? LOD : HalfLOD)); if (ValueAtACopy.IsEmpty() == ValueAtMiddle.IsEmpty()) { // If min and middle have same sign Min = Middle; ValueAtACopy = ValueAtMiddle; } else { // If max and middle have same sign Max = Middle; ValueAtBCopy = ValueAtMiddle; } checkError(Min <= Max); } const float Alpha = ValueAtACopy.ToFloat() / (ValueAtACopy.ToFloat() - ValueAtBCopy.ToFloat()); checkError(!FMath::IsNaN(Alpha) && FMath::IsFinite(Alpha)); const FIntVector GlobalMin = Local2DToGlobal( bIsAlongX ? Min : PositionA.X, bIsAlongY ? Min : PositionA.Y, 0); const FIntVector GlobalMax = Local2DToGlobal( bIsAlongX ? Max : PositionA.X, bIsAlongY ? Max : PositionA.Y, 0); IntersectionPoint = FMath::Lerp(FVector(GlobalMin), FVector(GlobalMax), Alpha); // Get intersection material if (!ValueAtACopy.IsEmpty()) { checkVoxelSlow(ValueAtBCopy.IsEmpty()); MaterialPosition = GlobalMin; } else { checkVoxelSlow(!ValueAtBCopy.IsEmpty()); MaterialPosition = GlobalMax; } } VertexIndex = Vertices.Num(); Vertices.Emplace(T(IntersectionPoint, MaterialPosition, bIsLowResChunk)); // If own vertex, save it if (CacheDirection & 0x08) { Cache2D[GetCacheIndex(EdgeIndex, LX, LY)] = VertexIndex; } } VertexIndices[i] = VertexIndex; checkVoxelSlow(0 <= VertexIndex && VertexIndex < Vertices.Num()); } // Add triangles // 3 vertex per triangle const int32 NumIndices = 3 * CellData.GetTriangleCount(); if (bFlip) { for (int32 Index = 0; Index < NumIndices; Index += 3) { Indices.Add(VertexIndices[CellData.vertexIndex[NumIndices - 1 - (Index + 0)]]); Indices.Add(VertexIndices[CellData.vertexIndex[NumIndices - 1 - (Index + 1)]]); Indices.Add(VertexIndices[CellData.vertexIndex[NumIndices - 1 - (Index + 2)]]); } } else { for (int32 Index = 0; Index < NumIndices; Index += 3) { Indices.Add(VertexIndices[CellData.vertexIndex[Index + 0]]); Indices.Add(VertexIndices[CellData.vertexIndex[Index + 1]]); Indices.Add(VertexIndices[CellData.vertexIndex[Index + 2]]); } } } } } return true; } TVoxelSharedPtr FVoxelMarchingCubeTransitionsMesher::CreateFullChunkImpl(FVoxelMesherTimes& Times) { VOXEL_ASYNC_FUNCTION_COUNTER(); struct FLocalVertex { FVector Position; FIntVector MaterialPosition; bool bNeedToTranslateVertex; FLocalVertex() = default; FORCEINLINE FLocalVertex(const FVector& Position, const FIntVector& MaterialPosition, bool bNeedToTranslateVertex) : Position(Position) , MaterialPosition(MaterialPosition) , bNeedToTranslateVertex(bNeedToTranslateVertex) { } }; TArray Indices; TArray Vertices; if (!CreateGeometryTemplate(Times, Indices, Vertices)) { return {}; } TArray MesherVertices = FMarchingCubeHelpers::CreateMesherVertices(Vertices); MESHER_TIME_MATERIALS(MesherVertices.Num(), FMarchingCubeHelpers::ComputeMaterials(*this, MesherVertices, Vertices)); MESHER_TIME(Normals, FMarchingCubeHelpers::ComputeNormals(*this, MesherVertices, Indices)); UnlockData(); MESHER_TIME(UVs, FMarchingCubeHelpers::ComputeUVs(*this, MesherVertices)); // Can't translate if we don't have valid normals if (Settings.NormalConfig != EVoxelNormalConfig::FlatNormal && Settings.NormalConfig != EVoxelNormalConfig::NoNormal) { VOXEL_ASYNC_SCOPE_COUNTER("Translate Vertices"); for (int32 Index = 0; Index < Vertices.Num(); Index++) { if (Vertices[Index].bNeedToTranslateVertex) { auto& Vertex = MesherVertices[Index]; Vertex.Position = FVoxelMesherUtilities::GetTranslatedTransvoxel(Vertex.Position, Vertex.Normal, TransitionsMask, LOD); } } } // Important: sanitize AFTER translating! FVoxelMesherUtilities::SanitizeMesh(Indices, MesherVertices); return MESHER_TIME_RETURN(CreateChunk, FVoxelMesherUtilities::CreateChunkFromVertices(Settings, LOD, MoveTemp(Indices), MoveTemp(MesherVertices))); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FORCEINLINE int32 FVoxelMarchingCubeTransitionsMesher::GetCacheIndex(int32 EdgeIndex, int32 LX, int32 LY) { checkVoxelSlow(0 <= LX && LX < RENDER_CHUNK_SIZE); checkVoxelSlow(0 <= LY && LY < RENDER_CHUNK_SIZE); checkVoxelSlow(0 <= EdgeIndex && EdgeIndex < TRANSITION_EDGE_INDEX_COUNT); return EdgeIndex + LX * TRANSITION_EDGE_INDEX_COUNT + LY * TRANSITION_EDGE_INDEX_COUNT * RENDER_CHUNK_SIZE; } template FORCEINLINE FVoxelValue FVoxelMarchingCubeTransitionsMesher::GetValue(int32 X, int32 Y, int32 InLOD) const { const FIntVector GlobalPosition = Local2DToGlobal(X, Y, 0); return Accelerator->Get(ChunkPosition + GlobalPosition, InLOD); } template FORCEINLINE FIntVector FVoxelMarchingCubeTransitionsMesher::Local2DToGlobal(int32 X, int32 Y, int32 Z) const { switch (Direction) { case EVoxelDirectionFlag::XMin: return { Z, X, Y }; case EVoxelDirectionFlag::XMax: return { Size - Z, Y, X }; case EVoxelDirectionFlag::YMin: return { Y, Z, X }; case EVoxelDirectionFlag::YMax: return { X, Size - Z, Y }; case EVoxelDirectionFlag::ZMin: return { X, Y, Z }; case EVoxelDirectionFlag::ZMax: return { Y, X, Size - Z }; default: check(false); return {}; } } #undef checkError