// Copyright Epic Games, Inc. All Rights Reserved. #include "LandscapeEdgeFixup.h" #include "Containers/StridedView.h" #include "Landscape.h" #include "LandscapeComponent.h" #include "LandscapePrivate.h" #include "LandscapeGroup.h" #include "RenderingThread.h" #include "TextureResource.h" #include "Hash/xxhash.h" #include "Streaming/TextureMipDataProvider.h" #include "LandscapeTextureHash.h" // enables verbose debug spew //#define ENABLE_LANDSCAPE_EDGE_FIXUP_DEBUG_SPEW 1 #ifdef ENABLE_LANDSCAPE_EDGE_FIXUP_DEBUG_SPEW #define FIXUP_DEBUG_LOG(...) UE_LOG(LogLandscape, Warning, __VA_ARGS__) #define FIXUP_DEBUG_LOG_PATCH(...) UE_LOG(LogLandscape, Warning, __VA_ARGS__) #define FIXUP_DEBUG_LOG_RENDER(...) UE_LOG(LogLandscape, Warning, __VA_ARGS__) #define FIXUP_DEBUG_LOG_DETAIL(...) UE_LOG(LogLandscape, Warning, __VA_ARGS__) #else #define FIXUP_DEBUG_LOG(...) UE_LOG(LogLandscape, Verbose, __VA_ARGS__) #define FIXUP_DEBUG_LOG_PATCH(...) do {} while(0) #define FIXUP_DEBUG_LOG_RENDER(...) do {} while(0) #define FIXUP_DEBUG_LOG_DETAIL(...) do {} while(0) #endif // ENABLE_LANDSCAPE_EDGE_FIXUP_DEBUG_SPEW namespace UE::Landscape { EDirectionFlags ToFlag(EDirectionIndex Index) { return static_cast(0x01 << static_cast(Index)); } EDirectionIndex GetOppositeIndex(EDirectionIndex Index) { uint32 OppositeIndex = (static_cast(Index) + 4) & 0x07; // add 4, mod 8 return static_cast(OppositeIndex); } FIntPoint GetNeighborRelativePosition(ENeighborIndex DirectionIndex) { static FIntPoint RelativePositions[static_cast(ENeighborIndex::Count)] = { FIntPoint(0, -1), // Bottom FIntPoint(1, -1), // Bottom Right FIntPoint(1, 0), // Right FIntPoint(1, 1), // Top Right FIntPoint(0, 1), // Top FIntPoint(-1, 1), // Top Left FIntPoint(-1, 0), // Left FIntPoint(-1, -1), // Bottom Left }; if ((DirectionIndex >= ENeighborIndex::First) && (DirectionIndex <= ENeighborIndex::Last)) { return RelativePositions[static_cast(DirectionIndex)]; } return FIntPoint(0, 0); } const FString& GetDirectionString(EDirectionIndex Index) { static const FString DirectionStrings[static_cast(EDirectionIndex::Count)] = { FString("B"), // Bottom FString("BR"), // Bottom Right FString("R"), // Right FString("TR"), // Top Right FString("T"), // Top FString("TL"), // Top Left FString("L"), // Left FString("BL"), // Bottom Left }; if ((Index >= EDirectionIndex::First) && (Index <= EDirectionIndex::Last)) { return DirectionStrings[static_cast(Index)]; } static const FString ErrorString("Error"); return ErrorString; } // returns true if the given direction is diagonal bool IsDiagonalCorner(EDirectionIndex DirectionIndex) { // cardinal directions have first bit zero, diagonals one return (static_cast(DirectionIndex) & 0x01) == 1; } bool IsTopOrBottomEdge(EEdgeIndex EdgeIndex) { // top and bottom are (index mod 4 == 0) return (static_cast(EdgeIndex) & 0x03) == 0; } bool IsDoubleTriangleCorner(EEdgeIndex EdgeIndex) { // double corner triangles are top-right (3) and bottom-left (7) return (static_cast(EdgeIndex) & 0x02) == 0; } // this converts a set of edge flags into a set of neighbors that are blended with those edges ENeighborFlags EdgesToAffectedNeighbors(EEdgeFlags EdgeFlags) { // changed edges only affect the corresponding neighbor // changed corners affect the corner neighbor AND the adjacent edge neighbors as well uint32 RawFlags = static_cast(EdgeFlags); uint32 CornerFlags = RawFlags & static_cast(ENeighborFlags::AllCorners); uint32 AdjacentA = (CornerFlags << 1) | (CornerFlags >> 7); // rotate left one (mod 8, applied below) uint32 AdjacentB = (CornerFlags << 7) | (CornerFlags >> 1); // rotate right one (mod 8, applied below) return static_cast((RawFlags | AdjacentA | AdjacentB) & static_cast(ENeighborFlags::All)); } // this converts a set of neighbors into the local edges that they blend with // (i.e. one of the neighbor's corners/edges is blended with the local edge) EEdgeFlags NeighborsToBlendedEdges(ENeighborFlags NeighborFlags) { // corner neighbors only blend with the corresponding corner // edge neighbors blend the corresponding edge AND also blend with the adjacent corners uint32 RawFlags = static_cast(NeighborFlags); uint32 EdgeFlags = RawFlags & static_cast(EEdgeFlags::AllEdges); uint32 AdjacentA = (EdgeFlags << 1) | (EdgeFlags >> 7); // rotate left one (mod 8, applied below) uint32 AdjacentB = (EdgeFlags << 7) | (EdgeFlags >> 1); // rotate right one (mod 8, applied below) return static_cast((RawFlags | AdjacentA | AdjacentB) & static_cast(EEdgeFlags::All)); } // select the direction rotated clockwise (45 degrees * ClockwiseRotationOffset) // this works for positive or negative offsets EDirectionIndex RotateDirection(EDirectionIndex NeighborIndex, int32 ClockwiseRotationOffset) { int32 Index = static_cast(NeighborIndex); int32 AdjacentIndex = (Index - ClockwiseRotationOffset) & (static_cast(EDirectionIndex::Count) - 1); return static_cast(AdjacentIndex); } EDirectionFlags RotateFlags(EDirectionFlags DirFlags, int32 ClockwiseRotationOffset) { uint32 FlagBits = (uint32) DirFlags; FlagBits = FlagBits | (FlagBits << 8); FlagBits = (FlagBits >> (ClockwiseRotationOffset & 0x07)) & 0xFF; return static_cast(FlagBits); } void GetSourceTextureEdgeStartCoord(EEdgeIndex EdgeIndex, int32 EdgeLength, int32& OutStartX, int32& OutStartY) { // StartX StartY Stride Count Order // Bottom = 0, 0 0 1 EdgeLength left to right // BottomRight = 1, EL-1 0 0 1 // Right = 2, EL-1 0 EL EdgeLength bottom to top // TopRight = 3, EL-1 EL-1 0 1 // Top = 4, 0 EL-1 1 EdgeLength left to right // TopLeft = 5, 0 EL-1 0 1 // Left = 6, 0 0 EL EdgeLength bottom to top // BottomLeft = 7, 0 0 0 1 check((EdgeIndex >= EEdgeIndex::First) && (EdgeIndex <= EEdgeIndex::Last)); int32 Index = static_cast(EdgeIndex); OutStartX = (((Index + 7) & 0x07) < 3) ? (EdgeLength-1) : 0; OutStartY = (((Index + 5) & 0x07) < 3) ? (EdgeLength-1) : 0; } int32 GetSourceTextureEdgeStartStrideCount(EEdgeIndex EdgeIndex, int32 EdgeLength, int32& OutStride, int32& OutCount) { int32 StartX, StartY; GetSourceTextureEdgeStartCoord(EdgeIndex, EdgeLength, StartX, StartY); int32 Offset = StartY * EdgeLength + StartX; if (!IsDiagonalCorner(EdgeIndex)) { int32 Count = EdgeLength; int32 Stride = IsTopOrBottomEdge(EdgeIndex) ? 1 : EdgeLength; // exclude corners from the edge Offset += Stride; Count -= 2; OutStride = Stride; OutCount = Count; return Offset; } else { OutStride = 0; OutCount = 1; return Offset; } } const TArrayView AllEdgeNeighbors() { static const ENeighborIndex EdgeNeighbors[4] = { ENeighborIndex::Bottom, ENeighborIndex::Right, ENeighborIndex::Top, ENeighborIndex::Left, }; return TArrayView(&EdgeNeighbors[0], 4); } const TArrayView AllCornerNeighbors() { static const ENeighborIndex CornerNeighbors[4] = { ENeighborIndex::BottomRight, ENeighborIndex::TopRight, ENeighborIndex::TopLeft, ENeighborIndex::BottomLeft, }; return TArrayView(&CornerNeighbors[0], 4); } // size 8x8 texture (with mips) // // C T T T T T T C // L R // L R C T T C // L R L R C C // L R L R C C // L R C B B C // L R // C B B B B B B C // mip 0 mip 1 mip 2 // // data is stored in this order: edges are stored left to right and bottom to top (for left and right edges) // // EdgeSnapshot memory layout: // B B << mip 1 (4x4) 2 pixel edges, left to right // B B B B B B << mip 0 (8x8) 6 pixel edges, left to right // R R << mip 1 bottom to top // R R R R R R << mip 0 bottom to top // T T left to right // T T T T T T left to right // L L bottom to top // L L L L L L bottom to top // // C C C C bottom-right, top-right, top-left, bottom-left (no mips needed) int32 GetEdgeArrayMipOffset(int32 MipEdgeLength) { // mip size edge size end start(offset) // 2x2 0 0 0 // 4x4 2 2 0 // 8x8 6 8 2 // 16x16 14 22 8 // 32x32 30 52 22 // 64x64 62 114 52 // 128x128 126 240 114 // 256x256 254 494 240 if (MipEdgeLength < 4) { return 0; } check((MipEdgeLength & (MipEdgeLength - 1)) == 0); // is power of two int32 Log2 = FMath::FloorLog2(MipEdgeLength); return MipEdgeLength - Log2 * 2; } int32 GetEdgeDataSize(int32 EdgeLength) { int32 DirectionSize = GetEdgeArrayMipOffset(EdgeLength * 2); return 4 * DirectionSize; } } TArrayView FHeightmapTextureEdgeSnapshot::GetEdgeData(UE::Landscape::EEdgeIndex InEdgeIndex, int32 InMipIndex) { using namespace UE::Landscape; check((InEdgeIndex >= EEdgeIndex::First) && (InEdgeIndex <= EEdgeIndex::Last)); check(!IsDiagonalCorner(InEdgeIndex)); { const int32 DirectionSize = EdgeData.Num() / 4; const int32 MipEdgeLength = EdgeLength >> InMipIndex; const int32 MipOffset = GetEdgeArrayMipOffset(MipEdgeLength); const int32 DirOffset = DirectionSize * ((int32)InEdgeIndex / 2); const int32 EdgeCount = MipEdgeLength - 2; check(DirOffset + MipOffset + EdgeCount <= EdgeData.Num()); // check data exists in the buffer return TArrayView((FHeightmapTexel*) &EdgeData[DirOffset + MipOffset], EdgeCount); } } UE::Landscape::FHeightmapTexel FHeightmapTextureEdgeSnapshot::GetCornerData(UE::Landscape::EEdgeIndex InCornerIndex) { using namespace UE::Landscape; check((InCornerIndex >= EEdgeIndex::First) && (InCornerIndex <= EEdgeIndex::Last)); check(IsDiagonalCorner(InCornerIndex)); // The snapshot corner hashes ARE the corner data (since they are the same size) FHeightmapTexel Texel; Texel.Data32 = SnapshotEdgeHashes[(int32)InCornerIndex]; return Texel; } TSharedRef FHeightmapTextureEdgeSnapshot::CreateEdgeSnapshotFromTextureData(const TArrayView64& InHeightmapTextureData, int32 InEdgeLength, const FVector& LandscapeGridScale) { TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeHeightmapTextureEdgeSnapshot::CreateEdgeDataFromTextureData); TSharedRef NewEdgeSnapshot = MakeShared(); NewEdgeSnapshot->CaptureEdgeDataFromTextureData_Internal(InHeightmapTextureData, InEdgeLength, LandscapeGridScale); return NewEdgeSnapshot; } #if WITH_EDITOR TSharedRef FHeightmapTextureEdgeSnapshot::CreateEdgeSnapshotFromHeightmapSource(UTexture2D* InHeightmap, const FVector& LandscapeGridScale) { TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeHeightmapTextureEdgeSnapshot::CreateEdgeSnapshotFromHeightmapSource); TSharedRef NewEdgeSnapshot = MakeShared(); NewEdgeSnapshot->CaptureEdgeDataFromHeightmapSource_Internal(InHeightmap, LandscapeGridScale); return NewEdgeSnapshot; } #endif // WITH_EDITOR UE::Landscape::EEdgeFlags FHeightmapTextureEdgeSnapshot::CompareEdges(const FHeightmapTextureEdgeSnapshot& Other) const { using namespace UE::Landscape; EEdgeFlags Changed = EEdgeFlags::None; for (EEdgeIndex EdgeIndex : TEnumRange()) { EEdgeFlags EdgeFlag = ToFlag(EdgeIndex); if (SnapshotEdgeHashes[(int32)EdgeIndex] != Other.SnapshotEdgeHashes[(int32)EdgeIndex] || InitialEdgeHashes[(int32)EdgeIndex] != Other.InitialEdgeHashes[(int32)EdgeIndex]) { Changed |= EdgeFlag; } } return Changed; } void FHeightmapTextureEdgeSnapshot::ResizeForEdgeLength(const int32 InEdgeLength) { if (EdgeLength != InEdgeLength) { EdgeLength = InEdgeLength; const int32 EdgeDataSize = UE::Landscape::GetEdgeDataSize(EdgeLength); EdgeData.SetNumZeroed(EdgeDataSize); } } #if WITH_EDITOR void FHeightmapTextureEdgeSnapshot::CaptureEdgeDataFromHeightmapSource_Internal(UTexture2D* InHeightmap, const FVector& LandscapeGridScale) { using namespace UE::Landscape; FTextureSource& Source = InHeightmap->Source; check(Source.IsValid()); check(Source.GetSizeX() == Source.GetSizeY()); TArray64 MipData; verify(Source.GetMipData(MipData, 0)); TextureSourceID = ULandscapeTextureHash::GetHash(InHeightmap); FIXUP_DEBUG_LOG_DETAIL(TEXT(" CaptureEdgeDataFromHeightmapSource_Internal %p (%s) - ID %s"), InHeightmap, *LandscapeGridScale.ToString(), *TextureSourceID.ToString()); TArrayView64 TexelData((FHeightmapTexel*) MipData.GetData(), MipData.Num() / sizeof(FHeightmapTexel)); CaptureEdgeDataFromTextureData_Internal(TexelData, Source.GetSizeY(), LandscapeGridScale); } #endif // WITH_EDITOR void FHeightmapTextureEdgeSnapshot::CaptureSingleEdgeDataAndComputeNormalsAndHashes( const UE::Landscape::FHeightmapTexel* TextureData, const UE::Landscape::EEdgeIndex EdgeOrCorner, const FVector& LandscapeGridScale) { using namespace UE::Landscape; // compute texture source stats int32 SourceStride = 0; int32 SourceCount = 0; const int32 SourceOffset = GetSourceTextureEdgeStartStrideCount(EdgeOrCorner, EdgeLength, SourceStride, SourceCount); const FHeightmapTexel* Src = &TextureData[SourceOffset]; // first pixel of the specified edge or corner in the source data int32 SrcLineStrideTexels = EdgeLength; // The triangle topology for each quad is: // // 00 ------ 10 // | \ | // | \ | // | \ | // | NB \ NT | // | \ | // | \ | // | \ | // 01 ------ 11 // // We compute VertexNormals by considering every quad that borders the desired edge or corner, and accumulating the NT/NB normals into the neighboring vertices auto CalculateNormalsForQuadAt = [SrcLineStrideTexels, LandscapeGridScale](const FHeightmapTexel* QuadTopLeftSrcPixel, FVector& OutNT, FVector& OutNB) { auto GetHeight = [SrcLineStrideTexels](const FHeightmapTexel* SrcPixel, int32 DX, int32 DY) { return SrcPixel[DY * SrcLineStrideTexels + DX].GetHeight16(); }; uint16 Height00 = GetHeight(QuadTopLeftSrcPixel, 0, 0); uint16 Height01 = GetHeight(QuadTopLeftSrcPixel, 0, 1); uint16 Height10 = GetHeight(QuadTopLeftSrcPixel, 1, 0); uint16 Height11 = GetHeight(QuadTopLeftSrcPixel, 1, 1); const FVector Vert00 = FVector(0.0f, 0.0f, LandscapeDataAccess::GetLocalHeight(Height00)) * LandscapeGridScale; const FVector Vert01 = FVector(0.0f, 1.0f, LandscapeDataAccess::GetLocalHeight(Height01)) * LandscapeGridScale; const FVector Vert10 = FVector(1.0f, 0.0f, LandscapeDataAccess::GetLocalHeight(Height10)) * LandscapeGridScale; const FVector Vert11 = FVector(1.0f, 1.0f, LandscapeDataAccess::GetLocalHeight(Height11)) * LandscapeGridScale; // top and bottom triangle normals OutNT = ((Vert00 - Vert10) ^ (Vert10 - Vert11)).GetSafeNormal(); OutNB = ((Vert11 - Vert01) ^ (Vert01 - Vert00)).GetSafeNormal(); }; // StartX StartY Stride Count Order // Bottom = 0, 0 0 1 EdgeLength left to right // BottomRight = 1, EL-1 0 0 1 // Right = 2, EL-1 0 EL EdgeLength bottom to top // TopRight = 3, EL-1 EL-1 0 1 // Top = 4, 0 EL-1 1 EdgeLength left to right // TopLeft = 5, 0 EL-1 0 1 // Left = 6, 0 0 EL EdgeLength bottom to top // BottomLeft = 7, 0 0 0 1 //the edges do not include the corner vertices, but the edge normals still need to consider the corner quads: // // BL B0 B1 BR // v - v - v - v // | \ | \ | \ | // L0 v - v - v - v R0 // | \ | \ | \ | // L1 v - v - v - v R1 // | \ | \ | \ | // v - v - v - v // TL T0 T1 TR uint32 InitialHash; uint32 SnapshotHash; if (IsDiagonalCorner(EdgeOrCorner)) { check(SourceCount == 1); const FHeightmapTexel* QuadTopLeftSrcPixel = Src; FVector NT, NB, VertexNormal; switch (EdgeOrCorner) { case EEdgeIndex::BottomRight: QuadTopLeftSrcPixel -= 1; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormal = NT; break; case EEdgeIndex::TopLeft: QuadTopLeftSrcPixel -= SrcLineStrideTexels; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormal = NB; break; case EEdgeIndex::TopRight: QuadTopLeftSrcPixel -= (SrcLineStrideTexels + 1); // fallthrough case EEdgeIndex::BottomLeft: CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormal = NT + NB; break; } // Note: Snapshot corner data is only stored in the SnapshotHash (since they are the same) FHeightmapTexel Dest; // setup normal data Dest.SetNormal(VertexNormal); // copy height data Dest.HeightL = Src->HeightL; Dest.HeightH = Src->HeightH; // hashes for corners are just a copy of the data (we take advantage of this in the hash comparison) InitialHash = Src->Data32; SnapshotHash = Dest.Data32; } else { const int32 MipZeroIndex = 0; TArrayView LocalEdgeData = GetEdgeData(EdgeOrCorner, MipZeroIndex); FHeightmapTexel* Dst = LocalEdgeData.GetData(); check(SourceCount == LocalEdgeData.Num()); const FHeightmapTexel* QuadTopLeftSrcPixel = Src; FVector NT, NB; TArray> VertexNormals; // if this ~12288 byte allocation blows the stack, we can convert it to a static array VertexNormals.SetNumZeroed(SourceCount, EAllowShrinking::No); switch (EdgeOrCorner) { case EEdgeIndex::Top: QuadTopLeftSrcPixel -= (SrcLineStrideTexels + 1); CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[0] += NT + NB; for (int32 i = 0; i < SourceCount - 1; ++i) { QuadTopLeftSrcPixel += 1; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[i] += NB; VertexNormals[i + 1] += NT + NB; } QuadTopLeftSrcPixel += 1; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[SourceCount - 1] += NB; break; case EEdgeIndex::Left: QuadTopLeftSrcPixel -= SrcLineStrideTexels; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[0] += NB; for (int32 i = 0; i < SourceCount - 1; ++i) { QuadTopLeftSrcPixel += SrcLineStrideTexels; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[i] += NT + NB; VertexNormals[i + 1] += NB; } QuadTopLeftSrcPixel += SrcLineStrideTexels; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[SourceCount - 1] += NT + NB; break; case EEdgeIndex::Bottom: QuadTopLeftSrcPixel -= 1; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[0] += NT; for (int32 i = 0; i < SourceCount - 1; ++i) { QuadTopLeftSrcPixel += 1; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[i] += NT + NB; VertexNormals[i + 1] += NT; } QuadTopLeftSrcPixel += 1; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[SourceCount - 1] += NT + NB; break; case EEdgeIndex::Right: QuadTopLeftSrcPixel -= (SrcLineStrideTexels + 1); CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[0] += NT + NB; for (int32 i = 0; i < SourceCount - 1; ++i) { QuadTopLeftSrcPixel += SrcLineStrideTexels; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[i] += NT; VertexNormals[i + 1] += NT + NB; } QuadTopLeftSrcPixel += SrcLineStrideTexels; CalculateNormalsForQuadAt(QuadTopLeftSrcPixel, NT, NB); VertexNormals[SourceCount - 1] += NT; break; } int32 SrcReadStrideTexels = IsTopOrBottomEdge(EdgeOrCorner) ? 1 : SrcLineStrideTexels; // edge hashes are the hash of each pixel in the edge FXxHash64Builder InitHashBuilder; FXxHash64Builder SnapHashBuilder; for (int32 i = 0; i < SourceCount; ++i) { // setup normal data Dst->SetNormal(VertexNormals[i]); // copy height data Dst->HeightH = Src->HeightH; Dst->HeightL = Src->HeightL; // update hashes InitHashBuilder.Update(Src, sizeof(FHeightmapTexel)); SnapHashBuilder.Update(Dst, sizeof(FHeightmapTexel)); Dst++; Src += SrcReadStrideTexels; } InitialHash = GetTypeHash(InitHashBuilder.Finalize()); SnapshotHash = GetTypeHash(SnapHashBuilder.Finalize()); // downsample to fill out the mip edge data { int32 MipCount = FMath::CeilLogTwo(EdgeLength) - 1; // we don't need to fill out the 1x1 or 2x2 mips as those have zero edge data.. TArrayView PrevMipEdge = LocalEdgeData; for (int32 MipIndex = 1; MipIndex < MipCount; MipIndex++) { TArrayView MipEdgeData = GetEdgeData(EdgeOrCorner, MipIndex); // the downsample pattern between mips: first and last elements get dropped, the rest have a 2 --> 1 averaging downsample applied // NOTE that edge downsampling is specifically designed to not pull in non-edge data (does not average with the "middle" of the heightmap) int32 MipEdgeLength = (PrevMipEdge.Num() - 2) / 2; check(MipEdgeLength == MipEdgeData.Num()); FHeightmapTexel* MDst = MipEdgeData.GetData(); FHeightmapTexel* MSrc = PrevMipEdge.GetData() + 1; // drop the first element for (int32 x = 0; x < MipEdgeLength; x++) { // blend normal MDst->NormalX = (MSrc[0].NormalX + (int32) MSrc[1].NormalX) / 2; MDst->NormalY = (MSrc[0].NormalY + (int32) MSrc[1].NormalY) / 2; // blend heights uint32 Height0 = MSrc[0].GetHeight16(); uint32 Height1 = MSrc[1].GetHeight16(); uint32 HeightBlended = (Height0 + Height1) / 2; MDst->SetHeight16(HeightBlended); MDst += 1; MSrc += 2; } PrevMipEdge = MipEdgeData; } } } uint32 OldInitialHash = InitialEdgeHashes[static_cast(EdgeOrCorner)]; uint32 OldSnapshotHash = SnapshotEdgeHashes[static_cast(EdgeOrCorner)]; FIXUP_DEBUG_LOG_DETAIL(TEXT(" - %s Hash i:%x s:%x --> i:%x s:%x"), *GetDirectionString(EdgeOrCorner), OldInitialHash, OldSnapshotHash, InitialHash, SnapshotHash); InitialEdgeHashes[static_cast(EdgeOrCorner)] = InitialHash; SnapshotEdgeHashes[static_cast(EdgeOrCorner)] = SnapshotHash; } void FHeightmapTextureEdgeSnapshot::CaptureEdgeDataFromTextureData_Internal(const TArrayView64& InHeightmapTextureData, int32 InEdgeLength, const FVector& LandscapeGridScale) { TRACE_CPUPROFILER_EVENT_SCOPE(FHeightmapTextureEdgeSnapshot::CaptureEdgeDataFromTextureData_Internal); using namespace UE::Landscape; check(InHeightmapTextureData.Num() == InEdgeLength * InEdgeLength); ResizeForEdgeLength(InEdgeLength); const FHeightmapTexel* TextureData = InHeightmapTextureData.GetData(); for (EEdgeIndex EdgeIndex : TEnumRange()) { CaptureSingleEdgeDataAndComputeNormalsAndHashes( TextureData, EdgeIndex, LandscapeGridScale); } FIXUP_DEBUG_LOG_DETAIL(TEXT(" CaptureEdgeDataFromTextureData_Internal --[%x %x %x %x %x %x %x %x | %x %x %x %x %x %x %x %x]--"), SnapshotEdgeHashes[0], SnapshotEdgeHashes[1], SnapshotEdgeHashes[2], SnapshotEdgeHashes[3], SnapshotEdgeHashes[4], SnapshotEdgeHashes[5], SnapshotEdgeHashes[6], SnapshotEdgeHashes[7], InitialEdgeHashes[0], InitialEdgeHashes[1], InitialEdgeHashes[2], InitialEdgeHashes[3], InitialEdgeHashes[4], InitialEdgeHashes[5], InitialEdgeHashes[6], InitialEdgeHashes[7]); return; } struct FHeightmapTextureEdgeSnapshotCustomVersion { enum Type { BeforeCustomVersionWasAdded = 0, BeforeInitialHashWasAdded = 1, BeforeCornerDataWasRemoved = 2, BeforeChangedCornerHash = 3, BeforeChangedCookedFormat = 4, LatestVersion = 5 }; static const FGuid GUID; }; const FGuid FHeightmapTextureEdgeSnapshotCustomVersion::GUID(0x12345678, 0x12345678, 0x12345678, 0x12345678); FCustomVersionRegistration GRegisterFHeightmapTextureEdgeSnapshotCustomVersion( FHeightmapTextureEdgeSnapshotCustomVersion::GUID, FHeightmapTextureEdgeSnapshotCustomVersion::LatestVersion, TEXT("FHeightmapTextureEdgeSnapshotCustomVersion")); FArchive& operator<<(FArchive& Ar, FHeightmapTextureEdgeSnapshot& EdgeSnapshot) { Ar.UsingCustomVersion(FHeightmapTextureEdgeSnapshotCustomVersion::GUID); int32 CustomVersion = Ar.CustomVer(FHeightmapTextureEdgeSnapshotCustomVersion::GUID); Ar << EdgeSnapshot.EdgeLength; Ar << EdgeSnapshot.EdgeData; if (CustomVersion <= FHeightmapTextureEdgeSnapshotCustomVersion::BeforeCornerDataWasRemoved) { TStaticArray CornerData = {}; Ar << CornerData; } Ar << EdgeSnapshot.SnapshotEdgeHashes; if (CustomVersion > FHeightmapTextureEdgeSnapshotCustomVersion::BeforeCustomVersionWasAdded) { #if WITH_EDITOR if (!Ar.IsCooking()) { EdgeSnapshot.TextureSourceID.Serialize(Ar); } #endif // WITH_EDITOR } if (CustomVersion > FHeightmapTextureEdgeSnapshotCustomVersion::BeforeInitialHashWasAdded) { Ar << EdgeSnapshot.InitialEdgeHashes; } #if WITH_EDITOR if (Ar.IsLoading() && CustomVersion < FHeightmapTextureEdgeSnapshotCustomVersion::LatestVersion) { // invalidate the guid so that we trigger re-capture of this snapshot with the latest snapshot code EdgeSnapshot.TextureSourceID.Invalidate(); } #endif // WITH_EDITOR return Ar; } ULandscapeHeightmapTextureEdgeFixup::ULandscapeHeightmapTextureEdgeFixup(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , EdgeSnapshot(MakeShared()) { } void ULandscapeHeightmapTextureEdgeFixup::SetHeightmapTexture(UTexture2D* InHeightmapTexture) { if (this->HeightmapTexture == nullptr) { // first time setup this->HeightmapTexture = InHeightmapTexture; // we assume the texture is in a pristine state the very first time // so we initialize the GPU edge hash to our initial edge hashes this->GPUEdgeHashes = EdgeSnapshot->InitialEdgeHashes; this->GPUEdgeModifiedFlags = UE::Landscape::EEdgeFlags::None; FIXUP_DEBUG_LOG_DETAIL(TEXT(" Set Initial GPU Edge Hashes: [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x]"), GPUEdgeHashes[0], GPUEdgeHashes[1], GPUEdgeHashes[2], GPUEdgeHashes[3], GPUEdgeHashes[4], GPUEdgeHashes[5], GPUEdgeHashes[6], GPUEdgeHashes[7]); } else { // not the first time, tracker may have existing state check(this->HeightmapTexture == InHeightmapTexture); // do not modify GPUEdgeHashes here, it records the existing state of the GPU texture (it may have already been modified) FIXUP_DEBUG_LOG_DETAIL(TEXT(" Repeat EdgeFixup Setup : no change GPU: [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x] CPU: [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x]"), GPUEdgeHashes[0], GPUEdgeHashes[1], GPUEdgeHashes[2], GPUEdgeHashes[3], GPUEdgeHashes[4], GPUEdgeHashes[5], GPUEdgeHashes[6], GPUEdgeHashes[7], EdgeSnapshot->SnapshotEdgeHashes[0], EdgeSnapshot->SnapshotEdgeHashes[1], EdgeSnapshot->SnapshotEdgeHashes[2], EdgeSnapshot->SnapshotEdgeHashes[3], EdgeSnapshot->SnapshotEdgeHashes[4], EdgeSnapshot->SnapshotEdgeHashes[5], EdgeSnapshot->SnapshotEdgeHashes[6], EdgeSnapshot->SnapshotEdgeHashes[7]); } } // set the active landscape component void ULandscapeHeightmapTextureEdgeFixup::SetActiveComponent(ULandscapeComponent* InComponent, FLandscapeGroup* InGroup, const bool bDisableCurrentActive) { check(HeightmapTexture != nullptr); if (ActiveComponent != nullptr) { check(ActiveGroup != nullptr); if (bDisableCurrentActive) { // deactivate the active component/group, add it to the disabled list ActiveGroup->DisableAndUnmap(this); FIXUP_DEBUG_LOG_DETAIL(TEXT(" DisableAndUnmap %p (%d,%d) edgefixup:%p MAPPED:%d TOTAL:%d"), ActiveComponent.Get(), GroupCoord.X, GroupCoord.Y, ActiveComponent->GetLandscapeProxy()->LODGroupKey, ActiveGroup->LandscapeGroupKey, this, ActiveGroup->XYToEdgeFixupMap.Num(), ActiveGroup->AllRegisteredFixups.Num()); } else { // unmap, don't flag as disabled ActiveGroup->Unmap(this); FIXUP_DEBUG_LOG_DETAIL(TEXT(" Unmap %p (%d,%d) edgefixup:%p MAPPED:%d TOTAL:%d"), ActiveComponent.Get(), GroupCoord.X, GroupCoord.Y, ActiveComponent->GetLandscapeProxy()->LODGroupKey, ActiveGroup->LandscapeGroupKey, this, ActiveGroup->XYToEdgeFixupMap.Num(), ActiveGroup->AllRegisteredFixups.Num()); } } else { check(ActiveGroup == nullptr); } ActiveComponent = InComponent; ActiveGroup = InGroup; if (InComponent != nullptr) { InGroup->Map(this, InComponent); FIXUP_DEBUG_LOG_DETAIL(TEXT(" Map %p (%d,%d) edgefixup:%p MAPPED:%d TOTAL:%d "), InComponent, GroupCoord.X, GroupCoord.Y, InComponent->GetLandscapeProxy()->LODGroupKey, InGroup->LandscapeGroupKey, this, InGroup->XYToEdgeFixupMap.Num(), InGroup->AllRegisteredFixups.Num()); } } void ULandscapeHeightmapTextureEdgeFixup::RequestEdgeTexturePatchingForNeighbors(UE::Landscape::ENeighborFlags NeighborsNeedingPatching) { using namespace UE::Landscape; check(bMapped); for (ENeighborIndex NeighborIndex : TEnumRange()) { if (EnumHasAnyFlags(NeighborsNeedingPatching, ToFlag(NeighborIndex))) { // a change to an edge or corner always affects the neighbor in the given direction if (ULandscapeHeightmapTextureEdgeFixup* Neighbor = ActiveGroup->GetNeighborEdgeFixup(GroupCoord, NeighborIndex)) { FIXUP_DEBUG_LOG_DETAIL(TEXT(" %s Neighbor %p (%d,%d) flagged for edge texture patching"), *GetDirectionString(NeighborIndex), Neighbor->ActiveComponent.Get(), Neighbor->GroupCoord.X, Neighbor->GroupCoord.Y); check(ActiveGroup->AllRegisteredFixups.Contains(Neighbor)); check(Neighbor->bMapped); ActiveGroup->HeightmapsNeedingEdgeTexturePatching.Add(Neighbor); } } } } void ULandscapeHeightmapTextureEdgeFixup::BlendCornerData(UE::Landscape::FHeightmapTexel& OutTexel, UE::Landscape::EEdgeIndex CornerIndex, const UE::Landscape::FNeighborSnapshots& NeighborSnapshots) { using namespace UE::Landscape; check(IsDiagonalCorner(CornerIndex)); uint32 NormalX = 0; uint32 NormalY = 0; uint32 Height = 0; uint32 SampleCount = 0; auto Accumulate = [&NormalX, &NormalY, &Height, &SampleCount](FHeightmapTextureEdgeSnapshot* Snapshot, EEdgeIndex EdgeIndex) { if (Snapshot != nullptr) { FHeightmapTexel CornerData = Snapshot->GetCornerData(EdgeIndex); // to match corner blend behavior, TL and BR are weighted 1, TR and BL are weighted 2 // because they have twice as many triangles incidient on the corner. if (IsDoubleTriangleCorner(EdgeIndex)) { NormalX += CornerData.NormalX; NormalY += CornerData.NormalY; Height += CornerData.GetHeight16(); SampleCount++; } else { NormalX += CornerData.NormalX * 2; NormalY += CornerData.NormalY * 2; Height += CornerData.GetHeight16() * 2; SampleCount += 2; } } }; FHeightmapTextureEdgeSnapshot* NeighborSnapshot = NeighborSnapshots.NeighborSnapshots[(int32)CornerIndex]; FHeightmapTextureEdgeSnapshot* NeighborEdgeASnapshot = NeighborSnapshots.NeighborSnapshots[(int32)RotateDirection(CornerIndex, 1)]; FHeightmapTextureEdgeSnapshot* NeighborEdgeBSnapshot = NeighborSnapshots.NeighborSnapshots[(int32)RotateDirection(CornerIndex, -1)]; Accumulate(NeighborSnapshots.LocalSnapshot, CornerIndex); Accumulate(NeighborSnapshot, GetOppositeIndex(CornerIndex)); Accumulate(NeighborEdgeASnapshot, RotateDirection(CornerIndex, -2)); Accumulate(NeighborEdgeBSnapshot, RotateDirection(CornerIndex, 2)); check(SampleCount >= 2); // at least we should have the local and one neighbor before callign this function OutTexel.NormalX = NormalX / SampleCount; OutTexel.NormalY = NormalY / SampleCount; OutTexel.SetHeight16(Height / SampleCount); } void ULandscapeHeightmapTextureEdgeFixup::BlendEdgeData(FHeightmapTextureEdgeSnapshot& EdgeSnapshot, UE::Landscape::EEdgeIndex EdgeIndex, int32 MipIndex, FHeightmapTextureEdgeSnapshot& NeighborEdgeSnapshot, TStridedView& OutDestView) { using namespace UE::Landscape; check(!IsDiagonalCorner(EdgeIndex)); TArrayView ThisEdgeData = EdgeSnapshot.GetEdgeData(EdgeIndex, MipIndex); TArrayView NeighborData = NeighborEdgeSnapshot.GetEdgeData(GetOppositeIndex(EdgeIndex), MipIndex); int32 Count = ThisEdgeData.Num(); check(Count == NeighborData.Num()); // allocate the buffer if necessary if (OutDestView.Num() == 0) { FHeightmapTexel* Buffer = new FHeightmapTexel[Count]; OutDestView = TStridedView(sizeof(FHeightmapTexel), Buffer, Count); } check(Count == OutDestView.Num()); // blend data between the edge and the neighboring edge const FHeightmapTexel* SrcE = &ThisEdgeData[0]; const FHeightmapTexel* SrcN = &NeighborData[0]; FHeightmapTexel* Dst = &OutDestView[0]; int32 DstStrideInTexels = OutDestView.GetStride() / sizeof(FHeightmapTexel); for (int32 p = 0; p < Count; p++) { Dst->NormalX = (SrcE->NormalX + (int32) SrcN->NormalX) / 2; Dst->NormalY = (SrcE->NormalY + (int32) SrcN->NormalY) / 2; // blend heights uint32 HeightE = SrcE->GetHeight16(); uint32 HeightN = SrcN->GetHeight16(); uint32 HeightBlended = (HeightE + HeightN) / 2; Dst->SetHeight16(HeightBlended); SrcE += 1; SrcN += 1; Dst += DstStrideInTexels; } } void ULandscapeHeightmapTextureEdgeFixup::GetNeighborSnapshots(UE::Landscape::FNeighborSnapshots &OutSnapshots) { using namespace UE::Landscape; check(ActiveGroup != nullptr); // should be caught before calling this function check(bMapped); ENeighborFlags ExistingNeighbors = ENeighborFlags::None; ENeighborFlags AnyModified = GPUEdgeModifiedFlags; for (ENeighborIndex NeighborIndex : TEnumRange()) { if (ULandscapeHeightmapTextureEdgeFixup* Neighbor = ActiveGroup->GetNeighborEdgeFixup(GroupCoord, NeighborIndex)) { ENeighborFlags NeighborFlags = ToFlag(NeighborIndex); ExistingNeighbors |= NeighborFlags; OutSnapshots.NeighborSnapshots[(int32)NeighborIndex] = &Neighbor->EdgeSnapshot.Get(); EEdgeFlags NeighborEdgeModifiedFlags = Neighbor->GPUEdgeModifiedFlags; if (EnumHasAnyFlags(NeighborEdgeModifiedFlags, ToFlag(GetOppositeIndex(NeighborIndex)))) { EnumAddFlags(AnyModified, NeighborFlags); } if (!IsDiagonalCorner(NeighborIndex)) { // this neighbor edge blends into our adjacent local corners EDirectionIndex RC = RotateDirection(NeighborIndex, 1); EDirectionIndex LC = RotateDirection(NeighborIndex, -1); if (EnumHasAnyFlags(NeighborEdgeModifiedFlags, ToFlag(GetOppositeIndex(RC)))) { EnumAddFlags(AnyModified, ToFlag(LC)); } if (EnumHasAnyFlags(NeighborEdgeModifiedFlags, ToFlag(GetOppositeIndex(LC)))) { EnumAddFlags(AnyModified, ToFlag(RC)); } } } else { OutSnapshots.NeighborSnapshots[(int32)NeighborIndex] = nullptr; } } OutSnapshots.ExistingNeighbors = ExistingNeighbors; OutSnapshots.EdgesWithAnyModifiedNeighbor = AnyModified; OutSnapshots.LocalSnapshot = &EdgeSnapshot.Get(); OutSnapshots.GPUEdgeHashes = GPUEdgeHashes; } int32 ULandscapeHeightmapTextureEdgeFixup::CheckAndPatchTextureEdgesFromEdgeSnapshots() { TRACE_CPUPROFILER_EVENT_SCOPE(CheckAndPatchTextureEdgesFromEdgeSnapshots); using namespace UE::Landscape; FNeighborSnapshots NeighborSnapshots; GetNeighborSnapshots(NeighborSnapshots); int32 PatchedEdgeCount = 0; // check any edges that may need to be patched, and patch them if necessary if (EnumHasAnyFlags(NeighborSnapshots.ExistingNeighbors, EEdgeFlags::AllEdges)) { for (EEdgeIndex EdgeIndex : AllEdgeNeighbors()) { FHeightmapTextureEdgeSnapshot* NeighborSnapshot = NeighborSnapshots.NeighborSnapshots[(int32)EdgeIndex]; if (NeighborSnapshot == nullptr) { FIXUP_DEBUG_LOG_PATCH(TEXT(" nopatch %s Edge - no neighbor"), *GetDirectionString(EdgeIndex)); continue; } const EEdgeFlags EdgeFlag = ToFlag(EdgeIndex); const bool bModified = EnumHasAnyFlags(NeighborSnapshots.EdgesWithAnyModifiedNeighbor, EdgeFlag); if (!bModified) { // with no modified edges affecting this edge, then we can compare initial states first, as it will likely be a match const uint32 LocalInitialHash = NeighborSnapshots.LocalSnapshot->InitialEdgeHashes[(int32)EdgeIndex]; const uint32 NeighborInitialHash = NeighborSnapshot->InitialEdgeHashes[(int32)GetOppositeIndex(EdgeIndex)]; check(LocalInitialHash == NeighborSnapshots.GPUEdgeHashes[(int32)EdgeIndex]); const bool bInitialStateMatch = (LocalInitialHash == NeighborInitialHash); if (bInitialStateMatch) { FIXUP_DEBUG_LOG_PATCH(TEXT(" nopatch %s Edge - initial state match"), *GetDirectionString(EdgeIndex)); continue; } } // lastly, check if this edge has already been patched to the desired state, by comparing hashes const uint32 LocalEdgeHash = NeighborSnapshots.LocalSnapshot->SnapshotEdgeHashes[(int32)EdgeIndex]; const uint32 NeighborEdgeHash = NeighborSnapshot->SnapshotEdgeHashes[(int32)GetOppositeIndex(EdgeIndex)]; const uint32 GPUEdgeHash = NeighborSnapshots.GPUEdgeHashes[(int32)EdgeIndex]; uint32 CombinedEdgeHash = HashCombineFast(LocalEdgeHash, NeighborEdgeHash); if (CombinedEdgeHash == GPUEdgeHash) { FIXUP_DEBUG_LOG_PATCH(TEXT(" nopatch %s Edge - modified match (%x,%x) (== old %x)"), *GetDirectionString(EdgeIndex), LocalEdgeHash, NeighborEdgeHash, GPUEdgeHash); continue; } FIXUP_DEBUG_LOG_PATCH(TEXT(" PATCHING %s Edge (hash %x | %x, new %x != old %x)"), *GetDirectionString(EdgeIndex), LocalEdgeHash, NeighborEdgeHash, CombinedEdgeHash, GPUEdgeHash); PatchTextureEdge_Internal(EdgeIndex); PatchedEdgeCount++; GPUEdgeHashes[(int32)EdgeIndex] = CombinedEdgeHash; EnumAddFlags(GPUEdgeModifiedFlags, EdgeFlag); } } // check any corners that may need to be patched, and patch them if necessary const EEdgeFlags CornersWithOneOrMoreNeighbors = NeighborsToBlendedEdges(NeighborSnapshots.ExistingNeighbors); if (EnumHasAnyFlags(CornersWithOneOrMoreNeighbors, EEdgeFlags::AllCorners)) { for (EEdgeIndex CornerIndex : AllCornerNeighbors()) { const EEdgeFlags CornerFlag = ToFlag(CornerIndex); // don't patch a corner unless at least one neighbor exists if (!EnumHasAnyFlags(CornersWithOneOrMoreNeighbors, CornerFlag)) { FIXUP_DEBUG_LOG_PATCH(TEXT(" nopatch %s Corner - no neighbors"), *GetDirectionString(CornerIndex)); continue; } const bool bModified = EnumHasAnyFlags(NeighborSnapshots.EdgesWithAnyModifiedNeighbor, CornerFlag); if (!bModified) { // if each edge's initial hash matches, no need to patch // (this is slightly different than the blend matching, as the blend may only include a subset of the edges, and produce a different result, // whereas the initial state may include data from edges that are not yet loaded) const uint32 LocalInitialHash = NeighborSnapshots.LocalSnapshot->InitialEdgeHashes[(int32)CornerIndex]; auto NeighborHasSameInitialHash = [LocalInitialHash, CornerIndex, NeighborSnapshots](int32 NeighborRotation, int32 NeighborEdgeRotation) -> bool { if (FHeightmapTextureEdgeSnapshot* Snapshot = NeighborSnapshots.NeighborSnapshots[(int32)RotateDirection(CornerIndex, NeighborRotation)]) { return Snapshot->InitialEdgeHashes[(int32)RotateDirection(CornerIndex, NeighborEdgeRotation)] == LocalInitialHash; } return true; }; const bool bInitialStateMatch = NeighborHasSameInitialHash(1, -2) && NeighborHasSameInitialHash(-1, 2) && NeighborHasSameInitialHash(0, 4); if (bInitialStateMatch) { FIXUP_DEBUG_LOG_PATCH(TEXT(" nopatch %s Corner - initial state match"), *GetDirectionString(CornerIndex)); continue; } } // Because the corner hashes are just a copy of the corner data, we can directly compare them with the expected result FHeightmapTexel Blended, GPUExisting = {}; BlendCornerData(Blended, CornerIndex, NeighborSnapshots); GPUExisting.Data32 = GPUEdgeHashes[(int32)CornerIndex]; if (Blended.Data32 == GPUExisting.Data32) { FIXUP_DEBUG_LOG_PATCH(TEXT(" nopatch %s Corner - exact match (%x == old %x)"), *GetDirectionString(CornerIndex), Blended.Data32, GPUExisting.Data32); continue; } // Check if we're only off by one in the normal, and don't bother patching these cases. // It might be noticeable with a very shiny material, but not in most landscape cases. // These cases generally happen due to floating point and round off error and inexactness between GPU and CPU implementations. if (Blended.IsSameHeight(GPUExisting) && (abs(Blended.NormalX - (int32)GPUExisting.NormalX) <= 1) && (abs(Blended.NormalY - (int32)GPUExisting.NormalY) <= 1)) { FIXUP_DEBUG_LOG_PATCH(TEXT(" nopatch %s Corner - off-by-one match (%x ~ old %x)"), *GetDirectionString(CornerIndex), Blended.Data32, GPUExisting.Data32); continue; } FIXUP_DEBUG_LOG_PATCH(TEXT(" PATCHING %s Corner (new %x != old %x)"), *GetDirectionString(CornerIndex), Blended.Data32, GPUExisting.Data32); PatchTextureCorner_Internal(CornerIndex, Blended); PatchedEdgeCount++; GPUEdgeHashes[(int32)CornerIndex] = Blended.Data32; EnumAddFlags(GPUEdgeModifiedFlags, CornerFlag); } } return PatchedEdgeCount; } void ULandscapeHeightmapTextureEdgeFixup::PatchTextureEdge_Internal(UE::Landscape::EEdgeIndex EdgeIndex) { TRACE_CPUPROFILER_EVENT_SCOPE(PatchTextureEdge_Internal); using namespace UE::Landscape; FTextureResource* HeightTextureResource = HeightmapTexture->GetResource(); if (!HeightTextureResource) { return; } #if WITH_EDITOR // this should be caught earlier -- we can't patch default textures check(!HeightmapTexture->IsDefaultTexture()); #endif // WITH_EDITOR check(!IsDiagonalCorner(EdgeIndex)); const int32 NumMips = HeightmapTexture->GetNumMips(); const int32 ResidentMips = HeightmapTexture->GetNumResidentMips(); ULandscapeHeightmapTextureEdgeFixup* EdgeNeighbor = ActiveGroup->GetNeighborEdgeFixup(GroupCoord, EdgeIndex); // the first (and largest) resident mip. For example, if all mips are resident, then this is mip 0. const int32 FirstResidentMipIndex = NumMips - ResidentMips; // this is the size of the first mip that has edges (2x2 and 1x1 mips don't have "edges", just "corners") const int32 MinMipSizeToFix = 4; // update this edge using data from neighbor and us int32 MipSize = EdgeSnapshot->EdgeLength >> FirstResidentMipIndex; for (int32 MipIndex = FirstResidentMipIndex; MipIndex < NumMips && MipSize >= MinMipSizeToFix; MipIndex++, MipSize /= 2) { // passing an empty TStridedView to BlendEdgeData will make it allocate a buffer to hold the blended data // (which we then delete in the render command below) TStridedView DestView; BlendEdgeData(EdgeSnapshot.Get(), EdgeIndex, MipIndex, EdgeNeighbor->EdgeSnapshot.Get(), DestView); ENQUEUE_RENDER_COMMAND(UpdateLandscapeHeightmapEdge)( [HeightTextureResource, DestView, EdgeIndex, MipSize, MipIndex, FirstResidentMipIndex, ResidentMips](FRHICommandListImmediate& RHICmdList) { const uint8* Buffer = DestView[0].Data; FTextureRHIRef RHIHeightmapTexture = HeightTextureResource->GetTextureRHI(); const FRHITextureDesc& Desc = RHIHeightmapTexture->GetDesc(); // Resource doesn't know about the unstreamed mips, so it's zero Mip starts at the first resident mip check(ResidentMips == Desc.NumMips); int32 ResourceMipIndex = MipIndex - FirstResidentMipIndex; if (ResourceMipIndex >= (int32) RHIHeightmapTexture->GetNumMips()) { UE_LOG(LogLandscape, Error, TEXT(" Can't Update Edge %d Mip %d because the texture resource does not have that mip"), EdgeIndex, ResourceMipIndex); } else { // determine the min/max range to update on the edge, based on whether we are updating the corners or not int32 DstStartX, DstStartY; int32 SrcStartX, SrcStartY, SrcWidth, SrcHeight; SrcStartX = 0; SrcStartY = 0; if (IsTopOrBottomEdge(EdgeIndex)) { DstStartX = 1; DstStartY = (EdgeIndex == EEdgeIndex::Bottom) ? 0 : (MipSize - 1); SrcWidth = MipSize - 2; SrcHeight = 1; } else { DstStartX = (EdgeIndex == EEdgeIndex::Left) ? 0 : (MipSize - 1); DstStartY = 1; SrcWidth = 1; SrcHeight = MipSize - 2; } { const uint32 SourcePitch = 4 * SrcWidth; FUpdateTextureRegion2D UpdateRegion(DstStartX, DstStartY, SrcStartX, SrcStartY, SrcWidth, SrcHeight); FIXUP_DEBUG_LOG_RENDER(TEXT(" PatchTextureEdge_Internal [RHIUpdateTexture2D] res:%p e:%d mip:%d reg:[%d %d %d %d %d %d]"), HeightTextureResource, EdgeIndex, MipIndex, UpdateRegion.DestX, UpdateRegion.DestY, UpdateRegion.SrcX, UpdateRegion.SrcY, UpdateRegion.Width, UpdateRegion.Height); RHIUpdateTexture2D(RHIHeightmapTexture, ResourceMipIndex, UpdateRegion, SourcePitch, Buffer); } } delete Buffer; } ); } } void ULandscapeHeightmapTextureEdgeFixup::PatchTextureCorner_Internal(UE::Landscape::EEdgeIndex CornerIndex, UE::Landscape::FHeightmapTexel Texel) { TRACE_CPUPROFILER_EVENT_SCOPE(PatchTextureCorner_Internal); using namespace UE::Landscape; FTextureResource* HeightTextureResource = HeightmapTexture->GetResource(); if (!HeightTextureResource) { return; } #if WITH_EDITOR // this should be caught earlier -- we can't patch default textures check(!HeightmapTexture->IsDefaultTexture()); #endif // WITH_EDITOR check(IsDiagonalCorner(CornerIndex)); const int32 NumMips = HeightmapTexture->GetNumMips(); const int32 ResidentMips = HeightmapTexture->GetNumResidentMips(); // the first (and largest) resident mip. For example, if all mips are resident, then this is mip 0. const int32 FirstResidentMipIndex = NumMips - ResidentMips; int32 FirstResidentMipSize = (EdgeSnapshot->EdgeLength >> FirstResidentMipIndex); ENQUEUE_RENDER_COMMAND(UpdateLandscapeHeightmapCorner)( [HeightTextureResource, Texel, CornerIndex, FirstResidentMipSize](FRHICommandListImmediate& RHICmdList) { FTextureRHIRef RHIHeightmapTexture = HeightTextureResource->GetTextureRHI(); const FRHITextureDesc& Desc = RHIHeightmapTexture->GetDesc(); const int32 NumMips = Desc.NumMips; // this is the size of the smallest mip that has 4 corners (1x1 mip only has one texel) const int32 MinMipSizeToFix = 2; int32 MipSize = FirstResidentMipSize; check(FirstResidentMipSize == Desc.GetSize().X); for (int32 ResourceMipIndex = 0; ResourceMipIndex < NumMips && MipSize >= MinMipSizeToFix; ResourceMipIndex++, MipSize /= 2) { // grab the corner coordinates in the texture int32 DstStartX, DstStartY; GetSourceTextureEdgeStartCoord(CornerIndex, MipSize, DstStartX, DstStartY); // update one pixel uint32 SourcePitch = 4; const int32 SrcStartX = 0; const int32 SrcStartY = 0; const int32 SrcWidth = 1; const int32 SrcHeight = 1; FUpdateTextureRegion2D UpdateRegion(DstStartX, DstStartY, SrcStartX, SrcStartY, SrcWidth, SrcHeight); FIXUP_DEBUG_LOG_RENDER(TEXT(" PatchTextureCorner_Internal [RHIUpdateTexture2D] res:%p e:%d mip:%d reg:[%d %d %d %d %d %d]"), HeightTextureResource, CornerIndex, ResourceMipIndex, UpdateRegion.DestX, UpdateRegion.DestY, UpdateRegion.SrcX, UpdateRegion.SrcY, UpdateRegion.Width, UpdateRegion.Height); RHIUpdateTexture2D(RHIHeightmapTexture, ResourceMipIndex, UpdateRegion, SourcePitch, Texel.Data); } } ); } int32 ULandscapeHeightmapTextureEdgeFixup::PatchTextureEdgesForStreamingMips(int32 FirstMipIndexInclusive, int32 LastMipIndexExclusive, FTextureMipInfoArray& DestMipInfos, const UE::Landscape::FNeighborSnapshots& NeighborSnapshots) { int32 PatchedEdges = 0; // TODO [chris.tchou] : it would be more efficient to iterate the mips internally to each edge, more similar to the non-streaming patch case for (int MipIndex = FirstMipIndexInclusive; MipIndex < LastMipIndexExclusive; MipIndex++) { FTextureMipInfo& DestMipInfo = DestMipInfos[MipIndex]; PatchedEdges += PatchTextureEdgesForSingleMip(MipIndex, DestMipInfo, NeighborSnapshots); } return PatchedEdges; } int32 ULandscapeHeightmapTextureEdgeFixup::PatchTextureEdgesForSingleMip(int32 MipIndex, FTextureMipInfo& DestMipInfo, const UE::Landscape::FNeighborSnapshots& NeighborSnapshots) { using namespace UE::Landscape; check(DestMipInfo.Format == PF_B8G8R8A8); // format used by FHeightmapTexel FHeightmapTexel* Dest = (FHeightmapTexel*) DestMipInfo.DestData; // some mip allocators do not provide RowPitch -- in that case assume SizeX texels is the pitch uint32 DstPitchInTexels; if (DestMipInfo.RowPitch > 0) { DstPitchInTexels = DestMipInfo.RowPitch / sizeof(FHeightmapTexel); } else { DstPitchInTexels = DestMipInfo.SizeX; } int32 MipEdgeLength = NeighborSnapshots.LocalSnapshot->EdgeLength >> MipIndex; check(DestMipInfo.SizeX >= (uint32) MipEdgeLength); check(DestMipInfo.SizeY >= (uint32) MipEdgeLength); int32 PatchedEdges = 0; if (EnumHasAnyFlags(NeighborSnapshots.EdgesWithAnyModifiedNeighbor, EEdgeFlags::AllEdges)) { for (EEdgeIndex EdgeIndex : AllEdgeNeighbors()) { // only patch edges that have been modified (to match the existing state of the other mips) if (!EnumHasAnyFlags(NeighborSnapshots.EdgesWithAnyModifiedNeighbor, ToFlag(EdgeIndex))) { continue; } // only patch edges that have an existing neighbor if (FHeightmapTextureEdgeSnapshot* NeighborSnapshot = NeighborSnapshots.NeighborSnapshots[(int32)EdgeIndex]) { // update this edge using data from neighbor and us TStridedView DestView; int32 CopyCount = MipEdgeLength - 2; if (IsTopOrBottomEdge(EdgeIndex)) { // horizontal int32 DstStartX = 1; int32 DstStartY = (EdgeIndex == EEdgeIndex::Bottom) ? 0 : (MipEdgeLength - 1); int32 DstOffset = DstStartX + DstStartY * DstPitchInTexels; DestView = TStridedView((int32)sizeof(FHeightmapTexel), &Dest[DstOffset], CopyCount); } else { // vertical int32 DstStartX = (EdgeIndex == EEdgeIndex::Left) ? 0 : (MipEdgeLength - 1); int32 DstStartY = 1; int32 DstOffset = DstStartX + DstStartY * DstPitchInTexels; DestView = TStridedView(DstPitchInTexels * sizeof(FHeightmapTexel), &Dest[DstOffset], CopyCount); } BlendEdgeData(*NeighborSnapshots.LocalSnapshot, EdgeIndex, MipIndex, *NeighborSnapshot, DestView); PatchedEdges++; FIXUP_DEBUG_LOG_RENDER(TEXT(" Patch Streamed Mip Edge:%s mip:%d len:%d"), *GetDirectionString(EdgeIndex), MipIndex, MipEdgeLength); } } } const EEdgeFlags CornersWithOneOrMoreNeighbors = NeighborsToBlendedEdges(NeighborSnapshots.ExistingNeighbors); if (EnumHasAnyFlags(CornersWithOneOrMoreNeighbors, EEdgeFlags::AllCorners)) { for (EEdgeIndex CornerIndex : AllCornerNeighbors()) { // only patch corners that have been modified (to match the existing state of the other mips) if (!EnumHasAnyFlags(NeighborSnapshots.EdgesWithAnyModifiedNeighbor, ToFlag(CornerIndex))) { continue; } // and corners that have at least one existing neighbor if (!EnumHasAnyFlags(CornersWithOneOrMoreNeighbors, ToFlag(CornerIndex))) { continue; } int32 DestStride = 0; int32 DestCount = 0; const int32 DestOffset = GetSourceTextureEdgeStartStrideCount(CornerIndex, MipEdgeLength, DestStride, DestCount); FHeightmapTexel &Texel = Dest[DestOffset]; BlendCornerData(Texel, CornerIndex, NeighborSnapshots); PatchedEdges++; FIXUP_DEBUG_LOG_RENDER(TEXT(" Patch Streamed Mip Corner:%s mip:%d len:%d"), *GetDirectionString(CornerIndex), MipIndex, MipEdgeLength); } } return PatchedEdges; } #if WITH_EDITOR void ULandscapeHeightmapTextureEdgeFixup::RequestEdgeSnapshotUpdateFromHeightmapSource(bool bInUpdateGPUEdgeHashes) { if (bInUpdateGPUEdgeHashes) { bUpdateGPUEdgeHashes = true; } check(ActiveGroup->AllRegisteredFixups.Contains(this)); ActiveGroup->HeightmapsNeedingEdgeSnapshotCapture.Add(this); } UE::Landscape::ENeighborFlags ULandscapeHeightmapTextureEdgeFixup::UpdateEdgeSnapshotFromHeightmapSource(const FVector& LandscapeGridScale, bool bForceUpdate) { TRACE_CPUPROFILER_EVENT_SCOPE(UpdateEdgeSnapshotFromHeightmapSource); using namespace UE::Landscape; ENeighborFlags ChangedEdges = ENeighborFlags::None; if (bForceUpdateSnapshot) { bForceUpdate = true; bForceUpdateSnapshot = false; } // no need to update if source ID matches (we assume texture has not actually changed) FGuid HeightmapSourceID = ULandscapeTextureHash::GetHash(HeightmapTexture); if (bForceUpdate || (HeightmapSourceID != EdgeSnapshot->TextureSourceID)) { FIXUP_DEBUG_LOG_DETAIL(TEXT(" UpdateEdgeSnapshotFromHeightmapSource %p HeightMap: %p -- UPDATED (ID: %s --> %s)"), ActiveComponent.Get(), HeightmapTexture.Get(), *EdgeSnapshot->TextureSourceID.ToString(), *HeightmapSourceID.ToString()); // create a new edge data TSharedRef NewEdgeSnapshot = FHeightmapTextureEdgeSnapshot::CreateEdgeSnapshotFromHeightmapSource(HeightmapTexture, LandscapeGridScale); // compare against previous edge data to see what edges have changed (and might cause neighbors to need to patch) ChangedEdges = NewEdgeSnapshot->CompareEdges(EdgeSnapshot.Get()); // assign the new edge data EdgeSnapshot = NewEdgeSnapshot; if (bUpdateGPUEdgeHashes) { GPUEdgeHashes = EdgeSnapshot->InitialEdgeHashes; GPUEdgeModifiedFlags = EEdgeFlags::None; FIXUP_DEBUG_LOG_DETAIL(TEXT(" Reset GPU Edge Hashes: [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x]"), GPUEdgeHashes[0], GPUEdgeHashes[1], GPUEdgeHashes[2], GPUEdgeHashes[3], GPUEdgeHashes[4], GPUEdgeHashes[5], GPUEdgeHashes[6], GPUEdgeHashes[7]); } } else { FIXUP_DEBUG_LOG_DETAIL(TEXT(" UpdateEdgeSnapshotFromHeightmapSource %p HeightMap: %p -- SourceID matches (%s)"), ActiveComponent.Get(), HeightmapTexture, *EdgeSnapshot->TextureSourceID.ToString()); } bUpdateGPUEdgeHashes = false; bDoNotPatchUntilGPUEdgeHashesUpdated = false; return ChangedEdges; } #endif // WITH_EDITOR void ULandscapeHeightmapTextureEdgeFixup::Serialize(FArchive& Ar) { if (Ar.IsLoading()) { // To ensure thread-safe CopyOnWrite behavior when loading, we need to allocate a new chunk of memory to serialize into, then replace the existing reference with that TSharedRef NewEdgeSnapshot = MakeShared(); Ar << NewEdgeSnapshot.Get(); EdgeSnapshot = NewEdgeSnapshot; FIXUP_DEBUG_LOG_DETAIL(TEXT("Loaded EdgeFixup - Snapshot [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x] (ID: %s)"), EdgeSnapshot->SnapshotEdgeHashes[0], EdgeSnapshot->SnapshotEdgeHashes[1], EdgeSnapshot->SnapshotEdgeHashes[2], EdgeSnapshot->SnapshotEdgeHashes[3], EdgeSnapshot->SnapshotEdgeHashes[4], EdgeSnapshot->SnapshotEdgeHashes[5], EdgeSnapshot->SnapshotEdgeHashes[6], EdgeSnapshot->SnapshotEdgeHashes[7], *EdgeSnapshot->GetTextureSourceIDAsString()); FIXUP_DEBUG_LOG_DETAIL(TEXT(" - Initial [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x]"), EdgeSnapshot->InitialEdgeHashes[0], EdgeSnapshot->InitialEdgeHashes[1], EdgeSnapshot->InitialEdgeHashes[2], EdgeSnapshot->InitialEdgeHashes[3], EdgeSnapshot->InitialEdgeHashes[4], EdgeSnapshot->InitialEdgeHashes[5], EdgeSnapshot->InitialEdgeHashes[6], EdgeSnapshot->InitialEdgeHashes[7]); FIXUP_DEBUG_LOG_DETAIL(TEXT(" - GPU [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x]"), GPUEdgeHashes[0], GPUEdgeHashes[1], GPUEdgeHashes[2], GPUEdgeHashes[3], GPUEdgeHashes[4], GPUEdgeHashes[5], GPUEdgeHashes[6], GPUEdgeHashes[7]); } else { Ar << EdgeSnapshot.Get(); FIXUP_DEBUG_LOG_DETAIL(TEXT(" Saved EdgeFixup - Snapshot [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x] (ID: %s)"), EdgeSnapshot->SnapshotEdgeHashes[0], EdgeSnapshot->SnapshotEdgeHashes[1], EdgeSnapshot->SnapshotEdgeHashes[2], EdgeSnapshot->SnapshotEdgeHashes[3], EdgeSnapshot->SnapshotEdgeHashes[4], EdgeSnapshot->SnapshotEdgeHashes[5], EdgeSnapshot->SnapshotEdgeHashes[6], EdgeSnapshot->SnapshotEdgeHashes[7], *EdgeSnapshot->GetTextureSourceIDAsString()); FIXUP_DEBUG_LOG_DETAIL(TEXT(" - Initial [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x]"), EdgeSnapshot->InitialEdgeHashes[0], EdgeSnapshot->InitialEdgeHashes[1], EdgeSnapshot->InitialEdgeHashes[2], EdgeSnapshot->InitialEdgeHashes[3], EdgeSnapshot->InitialEdgeHashes[4], EdgeSnapshot->InitialEdgeHashes[5], EdgeSnapshot->InitialEdgeHashes[6], EdgeSnapshot->InitialEdgeHashes[7]); FIXUP_DEBUG_LOG_DETAIL(TEXT(" - GPU [B:%x BR:%x R:%x TR:%x T:%x TL:%x L:%x BL:%x]"), GPUEdgeHashes[0], GPUEdgeHashes[1], GPUEdgeHashes[2], GPUEdgeHashes[3], GPUEdgeHashes[4], GPUEdgeHashes[5], GPUEdgeHashes[6], GPUEdgeHashes[7]); } } void ULandscapeHeightmapTextureEdgeFixup::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) { Super::AddReferencedObjects(InThis, Collector); ULandscapeHeightmapTextureEdgeFixup* const TypedThis = Cast(InThis); Collector.AddReferencedObject(TypedThis->HeightmapTexture); Collector.AddReferencedObject(TypedThis->ActiveComponent); } ULandscapeHeightmapTextureEdgeFixup* ULandscapeHeightmapTextureEdgeFixup::FindOrCreateFor(UTexture2D* InHeightmapTexture) { check(InHeightmapTexture); // try to get an existing factory ULandscapeHeightmapTextureEdgeFixup* Fixup = InHeightmapTexture->GetAssetUserData(); #if WITH_EDITORONLY_DATA if (Fixup == nullptr) { check(InHeightmapTexture->Source.IsValid()) check(InHeightmapTexture->Source.GetFormat() == TSF_BGRA8); // create a new Fixup, and attach it to the texture via User Data (and as outer) Fixup = NewObject(InHeightmapTexture); InHeightmapTexture->AddAssetUserData(Fixup); FIXUP_DEBUG_LOG_DETAIL(TEXT(" FindOrCreateFor texture %p -- CREATE FIXUP"), InHeightmapTexture); } else if (Fixup) { FIXUP_DEBUG_LOG_DETAIL(TEXT(" FindOrCreateFor texture %p -- FOUND FIXUP"), InHeightmapTexture); } else { FIXUP_DEBUG_LOG_DETAIL(TEXT(" FindOrCreateFor texture %p -- FAILED"), InHeightmapTexture); } #endif // WITH_EDITORONLY_DATA if (Fixup) { Fixup->SetHeightmapTexture(InHeightmapTexture); } return Fixup; } #undef FIXUP_DEBUG_LOG #undef FIXUP_DEBUG_LOG_PATCH #undef FIXUP_DEBUG_LOG_RENDER #undef FIXUP_DEBUG_LOG_DETAIL