You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
* SplineFalloffModulationTexture for Layer Info Object : we now log an error when applying the change and don't apply modulation in case the texture data is invalid * AlphaTexture for the alpha brush : we now log an error when setting the texture and revert to the default texture in case the texture data is invalid Misc: * Fixed undo/redo for Layer Info Object not triggering an update of the landscape splines #rb don.boogert #jira UE-162111 #preflight 633b4aa8ef773945652ac746 [CL 22366349 by jonathan bard in ue5-main branch]
1590 lines
48 KiB
C++
1590 lines
48 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "UObject/GCObject.h"
|
|
#include "LandscapeProxy.h"
|
|
#include "LandscapeToolInterface.h"
|
|
#include "LandscapeEdMode.h"
|
|
#include "EditorViewportClient.h"
|
|
#include "LandscapeEdit.h"
|
|
#include "LandscapeComponent.h"
|
|
#include "LandscapeDataAccess.h"
|
|
#include "LandscapeHeightfieldCollisionComponent.h"
|
|
#include "InstancedFoliageActor.h"
|
|
#include "AI/NavigationSystemBase.h"
|
|
#include "Landscape.h"
|
|
#include "LandscapeEditorPrivate.h"
|
|
#include "Logging/LogMacros.h"
|
|
#include "VisualLogger/VisualLogger.h"
|
|
|
|
|
|
//
|
|
// FNoiseParameter - Perlin noise
|
|
//
|
|
struct FNoiseParameter
|
|
{
|
|
float Base;
|
|
float NoiseScale;
|
|
float NoiseAmount;
|
|
|
|
// Constructors.
|
|
|
|
FNoiseParameter()
|
|
{
|
|
}
|
|
FNoiseParameter(float InBase, float InScale, float InAmount) :
|
|
Base(InBase),
|
|
NoiseScale(InScale),
|
|
NoiseAmount(InAmount)
|
|
{
|
|
}
|
|
|
|
// Sample
|
|
float Sample(int32 X, int32 Y) const
|
|
{
|
|
float Noise = 0.0f;
|
|
X = FMath::Abs(X);
|
|
Y = FMath::Abs(Y);
|
|
|
|
if (NoiseScale > DELTA)
|
|
{
|
|
for (uint32 Octave = 0; Octave < 4; Octave++)
|
|
{
|
|
float OctaveShift = 1 << Octave;
|
|
float OctaveScale = OctaveShift / NoiseScale;
|
|
Noise += PerlinNoise2D(X * OctaveScale, Y * OctaveScale) / OctaveShift;
|
|
}
|
|
}
|
|
|
|
return Base + Noise * NoiseAmount;
|
|
}
|
|
|
|
// TestGreater - Returns 1 if TestValue is greater than the parameter.
|
|
bool TestGreater(int32 X, int32 Y, float TestValue) const
|
|
{
|
|
float ParameterValue = Base;
|
|
|
|
if (NoiseScale > DELTA)
|
|
{
|
|
for (uint32 Octave = 0; Octave < 4; Octave++)
|
|
{
|
|
float OctaveShift = 1 << Octave;
|
|
float OctaveAmplitude = NoiseAmount / OctaveShift;
|
|
|
|
// Attempt to avoid calculating noise if the test value is outside of the noise amplitude.
|
|
|
|
if (TestValue > ParameterValue + OctaveAmplitude)
|
|
return 1;
|
|
else if (TestValue < ParameterValue - OctaveAmplitude)
|
|
return 0;
|
|
else
|
|
{
|
|
float OctaveScale = OctaveShift / NoiseScale;
|
|
ParameterValue += PerlinNoise2D(X * OctaveScale, Y * OctaveScale) * OctaveAmplitude;
|
|
}
|
|
}
|
|
}
|
|
|
|
return TestValue >= ParameterValue;
|
|
}
|
|
|
|
// TestLess
|
|
bool TestLess(int32 X, int32 Y, float TestValue) const { return !TestGreater(X, Y, TestValue); }
|
|
|
|
private:
|
|
static const int32 Permutations[256];
|
|
|
|
bool operator==(const FNoiseParameter& SrcNoise)
|
|
{
|
|
if ((Base == SrcNoise.Base) &&
|
|
(NoiseScale == SrcNoise.NoiseScale) &&
|
|
(NoiseAmount == SrcNoise.NoiseAmount))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void operator=(const FNoiseParameter& SrcNoise)
|
|
{
|
|
Base = SrcNoise.Base;
|
|
NoiseScale = SrcNoise.NoiseScale;
|
|
NoiseAmount = SrcNoise.NoiseAmount;
|
|
}
|
|
|
|
|
|
float Fade(float T) const
|
|
{
|
|
return T * T * T * (T * (T * 6 - 15) + 10);
|
|
}
|
|
|
|
|
|
float Grad(int32 Hash, float X, float Y) const
|
|
{
|
|
int32 H = Hash & 15;
|
|
float U = H < 8 || H == 12 || H == 13 ? X : Y,
|
|
V = H < 4 || H == 12 || H == 13 ? Y : 0;
|
|
return ((H & 1) == 0 ? U : -U) + ((H & 2) == 0 ? V : -V);
|
|
}
|
|
|
|
float PerlinNoise2D(float X, float Y) const
|
|
{
|
|
int32 TruncX = FMath::TruncToInt(X),
|
|
TruncY = FMath::TruncToInt(Y),
|
|
IntX = TruncX & 255,
|
|
IntY = TruncY & 255;
|
|
float FracX = X - TruncX,
|
|
FracY = Y - TruncY;
|
|
|
|
float U = Fade(FracX),
|
|
V = Fade(FracY);
|
|
|
|
int32 A = Permutations[IntX] + IntY,
|
|
AA = Permutations[A & 255],
|
|
AB = Permutations[(A + 1) & 255],
|
|
B = Permutations[(IntX + 1) & 255] + IntY,
|
|
BA = Permutations[B & 255],
|
|
BB = Permutations[(B + 1) & 255];
|
|
|
|
return FMath::Lerp(FMath::Lerp(Grad(Permutations[AA], FracX, FracY),
|
|
Grad(Permutations[BA], FracX - 1, FracY), U),
|
|
FMath::Lerp(Grad(Permutations[AB], FracX, FracY - 1),
|
|
Grad(Permutations[BB], FracX - 1, FracY - 1), U), V);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
#if WITH_KISSFFT
|
|
#include "tools/kiss_fftnd.h"
|
|
#endif
|
|
|
|
template<typename DataType>
|
|
inline void LowPassFilter(int32 X1, int32 Y1, int32 X2, int32 Y2, FLandscapeBrushData& BrushInfo, TArray<DataType>& Data, const float DetailScale, const float ApplyRatio = 1.0f)
|
|
{
|
|
#if WITH_KISSFFT
|
|
// Low-pass filter
|
|
int32 FFTWidth = X2 - X1 - 1;
|
|
int32 FFTHeight = Y2 - Y1 - 1;
|
|
|
|
if (FFTWidth <= 1 && FFTHeight <= 1)
|
|
{
|
|
// nothing to do
|
|
return;
|
|
}
|
|
|
|
const int32 NDims = 2;
|
|
const int32 Dims[NDims] = { FFTHeight, FFTWidth };
|
|
kiss_fftnd_cfg stf = kiss_fftnd_alloc(Dims, NDims, 0, NULL, NULL),
|
|
sti = kiss_fftnd_alloc(Dims, NDims, 1, NULL, NULL);
|
|
|
|
kiss_fft_cpx *buf = (kiss_fft_cpx *)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * Dims[0] * Dims[1]);
|
|
kiss_fft_cpx *out = (kiss_fft_cpx *)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * Dims[0] * Dims[1]);
|
|
|
|
for (int32 Y = Y1 + 1; Y <= Y2 - 1; Y++)
|
|
{
|
|
auto* DataScanline = Data.GetData() + (Y - Y1) * (X2 - X1 + 1) + (0 - X1);
|
|
auto* bufScanline = buf + (Y - (Y1 + 1)) * Dims[1] + (0 - (X1 + 1));
|
|
|
|
for (int32 X = X1 + 1; X <= X2 - 1; X++)
|
|
{
|
|
bufScanline[X].r = DataScanline[X];
|
|
bufScanline[X].i = 0;
|
|
}
|
|
}
|
|
|
|
// Forward FFT
|
|
kiss_fftnd(stf, buf, out);
|
|
|
|
int32 CenterPos[2] = { Dims[0] >> 1, Dims[1] >> 1 };
|
|
for (int32 Y = 0; Y < Dims[0]; Y++)
|
|
{
|
|
float DistFromCenter = 0.0f;
|
|
for (int32 X = 0; X < Dims[1]; X++)
|
|
{
|
|
if (Y < CenterPos[0])
|
|
{
|
|
if (X < CenterPos[1])
|
|
{
|
|
// 1
|
|
DistFromCenter = X*X + Y*Y;
|
|
}
|
|
else
|
|
{
|
|
// 2
|
|
DistFromCenter = (X - Dims[1])*(X - Dims[1]) + Y*Y;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (X < CenterPos[1])
|
|
{
|
|
// 3
|
|
DistFromCenter = X*X + (Y - Dims[0])*(Y - Dims[0]);
|
|
}
|
|
else
|
|
{
|
|
// 4
|
|
DistFromCenter = (X - Dims[1])*(X - Dims[1]) + (Y - Dims[0])*(Y - Dims[0]);
|
|
}
|
|
}
|
|
// High frequency removal
|
|
float Ratio = 1.0f - DetailScale;
|
|
float Dist = FMath::Min<float>((Dims[0] * Ratio)*(Dims[0] * Ratio), (Dims[1] * Ratio)*(Dims[1] * Ratio));
|
|
float Filter = 1.0 / (1.0 + DistFromCenter / Dist);
|
|
CA_SUPPRESS(6385);
|
|
out[X + Y*Dims[1]].r *= Filter;
|
|
out[X + Y*Dims[1]].i *= Filter;
|
|
}
|
|
}
|
|
|
|
// Inverse FFT
|
|
kiss_fftnd(sti, out, buf);
|
|
|
|
float Scale = Dims[0] * Dims[1];
|
|
const int32 BrushX1 = FMath::Max<int32>(BrushInfo.GetBounds().Min.X, X1 + 1);
|
|
const int32 BrushY1 = FMath::Max<int32>(BrushInfo.GetBounds().Min.Y, Y1 + 1);
|
|
const int32 BrushX2 = FMath::Min<int32>(BrushInfo.GetBounds().Max.X, X2);
|
|
const int32 BrushY2 = FMath::Min<int32>(BrushInfo.GetBounds().Max.Y, Y2);
|
|
for (int32 Y = BrushY1; Y < BrushY2; Y++)
|
|
{
|
|
const float* BrushScanline = BrushInfo.GetDataPtr(FIntPoint(0, Y));
|
|
auto* DataScanline = Data.GetData() + (Y - Y1) * (X2 - X1 + 1) + (0 - X1);
|
|
auto* bufScanline = buf + (Y - (Y1 + 1)) * Dims[1] + (0 - (X1 + 1));
|
|
|
|
for (int32 X = BrushX1; X < BrushX2; X++)
|
|
{
|
|
const float BrushValue = BrushScanline[X];
|
|
|
|
if (BrushValue > 0.0f)
|
|
{
|
|
DataScanline[X] = FMath::Lerp((float)DataScanline[X], bufScanline[X].r / Scale, BrushValue * ApplyRatio);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Free FFT allocation
|
|
KISS_FFT_FREE(stf);
|
|
KISS_FFT_FREE(sti);
|
|
KISS_FFT_FREE(buf);
|
|
KISS_FFT_FREE(out);
|
|
#endif
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// TLandscapeEditCache
|
|
//
|
|
template<class Accessor, typename AccessorType>
|
|
struct TLandscapeEditCache
|
|
{
|
|
public:
|
|
typedef AccessorType DataType;
|
|
Accessor DataAccess;
|
|
|
|
TLandscapeEditCache(const FLandscapeToolTarget& InTarget)
|
|
: DataAccess(InTarget)
|
|
, LandscapeInfo(InTarget.LandscapeInfo)
|
|
{
|
|
check(LandscapeInfo != nullptr);
|
|
}
|
|
|
|
// X2/Y2 Coordinates are "inclusive" max values
|
|
// Note that this should maybe be called "ExtendDataCache" because the region here will be combined with the existing cached region, not loaded independently, giving a cached region that is the bounding box of previous and new
|
|
void CacheData(int32 X1, int32 Y1, int32 X2, int32 Y2, bool bCacheOriginalData = false)
|
|
{
|
|
if (!bIsValid)
|
|
{
|
|
if (Accessor::bUseInterp)
|
|
{
|
|
ValidX1 = CachedX1 = X1;
|
|
ValidY1 = CachedY1 = Y1;
|
|
ValidX2 = CachedX2 = X2;
|
|
ValidY2 = CachedY2 = Y2;
|
|
|
|
DataAccess.GetData(ValidX1, ValidY1, ValidX2, ValidY2, CachedData);
|
|
if (!ensureMsgf(ValidX1 <= ValidX2 && ValidY1 <= ValidY2, TEXT("Invalid cache area: X(%d-%d), Y(%d-%d) from region X(%d-%d), Y(%d-%d)"), ValidX1, ValidX2, ValidY1, ValidY2, X1, X2, Y1, Y2))
|
|
{
|
|
bIsValid = false;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CachedX1 = X1;
|
|
CachedY1 = Y1;
|
|
CachedX2 = X2;
|
|
CachedY2 = Y2;
|
|
|
|
DataAccess.GetDataFast(CachedX1, CachedY1, CachedX2, CachedY2, CachedData);
|
|
}
|
|
|
|
// Drop a visual log to indicate the area covered by this cache region extension :
|
|
VisualizeLandscapeRegion(CachedX1, CachedY1, CachedX2, CachedY2, FColor::Red, TEXT("Cache Data"));
|
|
|
|
if (bCacheOriginalData)
|
|
{
|
|
OriginalData = CachedData;
|
|
}
|
|
bIsValid = true;
|
|
}
|
|
else
|
|
{
|
|
bool bCacheExtended = false;
|
|
|
|
// Extend the cache area if needed
|
|
if (X1 < CachedX1)
|
|
{
|
|
if (Accessor::bUseInterp)
|
|
{
|
|
int32 x1 = X1;
|
|
int32 x2 = ValidX1;
|
|
int32 y1 = FMath::Min<int32>(Y1, CachedY1);
|
|
int32 y2 = FMath::Max<int32>(Y2, CachedY2);
|
|
|
|
DataAccess.GetData(x1, y1, x2, y2, CachedData);
|
|
ValidX1 = FMath::Min<int32>(x1, ValidX1);
|
|
}
|
|
else
|
|
{
|
|
DataAccess.GetDataFast(X1, CachedY1, CachedX1 - 1, CachedY2, CachedData);
|
|
}
|
|
|
|
if (bCacheOriginalData)
|
|
{
|
|
CacheOriginalData(X1, CachedY1, CachedX1 - 1, CachedY2);
|
|
}
|
|
CachedX1 = X1;
|
|
|
|
bCacheExtended = true;
|
|
}
|
|
|
|
if (X2 > CachedX2)
|
|
{
|
|
if (Accessor::bUseInterp)
|
|
{
|
|
int32 x1 = ValidX2;
|
|
int32 x2 = X2;
|
|
int32 y1 = FMath::Min<int32>(Y1, CachedY1);
|
|
int32 y2 = FMath::Max<int32>(Y2, CachedY2);
|
|
|
|
DataAccess.GetData(x1, y1, x2, y2, CachedData);
|
|
ValidX2 = FMath::Max<int32>(x2, ValidX2);
|
|
}
|
|
else
|
|
{
|
|
DataAccess.GetDataFast(CachedX2 + 1, CachedY1, X2, CachedY2, CachedData);
|
|
}
|
|
if (bCacheOriginalData)
|
|
{
|
|
CacheOriginalData(CachedX2 + 1, CachedY1, X2, CachedY2);
|
|
}
|
|
CachedX2 = X2;
|
|
|
|
bCacheExtended = true;
|
|
}
|
|
|
|
if (Y1 < CachedY1)
|
|
{
|
|
if (Accessor::bUseInterp)
|
|
{
|
|
int32 x1 = CachedX1;
|
|
int32 x2 = CachedX2;
|
|
int32 y1 = Y1;
|
|
int32 y2 = ValidY1;
|
|
|
|
DataAccess.GetData(x1, y1, x2, y2, CachedData);
|
|
ValidY1 = FMath::Min<int32>(y1, ValidY1);
|
|
}
|
|
else
|
|
{
|
|
DataAccess.GetDataFast(CachedX1, Y1, CachedX2, CachedY1 - 1, CachedData);
|
|
}
|
|
if (bCacheOriginalData)
|
|
{
|
|
CacheOriginalData(CachedX1, Y1, CachedX2, CachedY1 - 1);
|
|
}
|
|
CachedY1 = Y1;
|
|
|
|
bCacheExtended = true;
|
|
}
|
|
|
|
if (Y2 > CachedY2)
|
|
{
|
|
if (Accessor::bUseInterp)
|
|
{
|
|
int32 x1 = CachedX1;
|
|
int32 x2 = CachedX2;
|
|
int32 y1 = ValidY2;
|
|
int32 y2 = Y2;
|
|
|
|
DataAccess.GetData(x1, y1, x2, y2, CachedData);
|
|
ValidY2 = FMath::Max<int32>(y2, ValidY2);
|
|
}
|
|
else
|
|
{
|
|
DataAccess.GetDataFast(CachedX1, CachedY2 + 1, CachedX2, Y2, CachedData);
|
|
}
|
|
if (bCacheOriginalData)
|
|
{
|
|
CacheOriginalData(CachedX1, CachedY2 + 1, CachedX2, Y2);
|
|
}
|
|
CachedY2 = Y2;
|
|
|
|
bCacheExtended = true;
|
|
}
|
|
|
|
if (bCacheExtended)
|
|
{
|
|
// Drop a visual log to indicate the area covered by this cache region extension :
|
|
VisualizeLandscapeRegion(CachedX1, CachedY1, CachedX2, CachedY2, FColor::Red, TEXT("Cache Data"));
|
|
}
|
|
}
|
|
}
|
|
|
|
AccessorType* GetValueRef(int32 LandscapeX, int32 LandscapeY)
|
|
{
|
|
return CachedData.Find(FIntPoint(LandscapeX, LandscapeY));
|
|
}
|
|
|
|
float GetValue(float LandscapeX, float LandscapeY)
|
|
{
|
|
int32 X = FMath::FloorToInt(LandscapeX);
|
|
int32 Y = FMath::FloorToInt(LandscapeY);
|
|
AccessorType* P00 = CachedData.Find(FIntPoint(X, Y));
|
|
AccessorType* P10 = CachedData.Find(FIntPoint(X + 1, Y));
|
|
AccessorType* P01 = CachedData.Find(FIntPoint(X, Y + 1));
|
|
AccessorType* P11 = CachedData.Find(FIntPoint(X + 1, Y + 1));
|
|
|
|
// Search for nearest value if missing data
|
|
float V00 = P00 ? *P00 : (P10 ? *P10 : (P01 ? *P01 : (P11 ? *P11 : 0.0f)));
|
|
float V10 = P10 ? *P10 : (P00 ? *P00 : (P11 ? *P11 : (P01 ? *P01 : 0.0f)));
|
|
float V01 = P01 ? *P01 : (P00 ? *P00 : (P11 ? *P11 : (P10 ? *P10 : 0.0f)));
|
|
float V11 = P11 ? *P11 : (P10 ? *P10 : (P01 ? *P01 : (P00 ? *P00 : 0.0f)));
|
|
|
|
return FMath::Lerp(
|
|
FMath::Lerp(V00, V10, LandscapeX - X),
|
|
FMath::Lerp(V01, V11, LandscapeX - X),
|
|
LandscapeY - Y);
|
|
}
|
|
|
|
FVector GetNormal(int32 X, int32 Y)
|
|
{
|
|
AccessorType* P00 = CachedData.Find(FIntPoint(X, Y));
|
|
AccessorType* P10 = CachedData.Find(FIntPoint(X + 1, Y));
|
|
AccessorType* P01 = CachedData.Find(FIntPoint(X, Y + 1));
|
|
AccessorType* P11 = CachedData.Find(FIntPoint(X + 1, Y + 1));
|
|
|
|
// Search for nearest value if missing data
|
|
float V00 = P00 ? *P00 : (P10 ? *P10 : (P01 ? *P01 : (P11 ? *P11 : 0.0f)));
|
|
float V10 = P10 ? *P10 : (P00 ? *P00 : (P11 ? *P11 : (P01 ? *P01 : 0.0f)));
|
|
float V01 = P01 ? *P01 : (P00 ? *P00 : (P11 ? *P11 : (P10 ? *P10 : 0.0f)));
|
|
float V11 = P11 ? *P11 : (P10 ? *P10 : (P01 ? *P01 : (P00 ? *P00 : 0.0f)));
|
|
|
|
FVector Vert00 = FVector(0.0f, 0.0f, V00);
|
|
FVector Vert01 = FVector(0.0f, 1.0f, V01);
|
|
FVector Vert10 = FVector(1.0f, 0.0f, V10);
|
|
FVector Vert11 = FVector(1.0f, 1.0f, V11);
|
|
|
|
FVector FaceNormal1 = ((Vert00 - Vert10) ^ (Vert10 - Vert11)).GetSafeNormal();
|
|
FVector FaceNormal2 = ((Vert11 - Vert01) ^ (Vert01 - Vert00)).GetSafeNormal();
|
|
return (FaceNormal1 + FaceNormal2).GetSafeNormal();
|
|
}
|
|
|
|
void SetValue(int32 LandscapeX, int32 LandscapeY, AccessorType Value)
|
|
{
|
|
CachedData.Add(FIntPoint(LandscapeX, LandscapeY), Forward<AccessorType>(Value));
|
|
}
|
|
|
|
bool IsZeroValue(const FVector& Value)
|
|
{
|
|
return (FMath::IsNearlyZero(Value.X) && FMath::IsNearlyZero(Value.Y));
|
|
}
|
|
|
|
bool IsZeroValue(const FVector2D& Value)
|
|
{
|
|
return (FMath::IsNearlyZero(Value.X) && FMath::IsNearlyZero(Value.Y));
|
|
}
|
|
|
|
bool IsZeroValue(const uint16& Value)
|
|
{
|
|
return Value == 0;
|
|
}
|
|
|
|
bool IsZeroValue(const uint8& Value)
|
|
{
|
|
return Value == 0;
|
|
}
|
|
|
|
bool HasCachedData(int32 X1, int32 Y1, int32 X2, int32 Y2) const
|
|
{
|
|
return (bIsValid && X1 >= CachedX1 && Y1 >= CachedY1 && X2 <= CachedX2 && Y2 <= CachedY2);
|
|
}
|
|
|
|
using FPrepareRegionForCachingFunction = TFunction<FIntRect(const FIntRect& NewCacheBounds)>;
|
|
bool GetDataAndCache(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<AccessorType>& OutData, FPrepareRegionForCachingFunction PrepareRegionForCaching)
|
|
{
|
|
// Drop a visual log to indicate the area requested by this data access :
|
|
VisualizeLandscapeRegion(X1, Y1, X2, Y2, FColor::Blue, TEXT("CacheDataRequest"));
|
|
|
|
if (!HasCachedData(X1, Y1, X2, Y2))
|
|
{
|
|
// The cache needs to be expanded, compute the new bounds :
|
|
// The bounds we calculate here need to be what would be the result of calling CacheData with this region, meaning that they should include the previous bounds. This will let us pass the correct region of interest to PrepareRegionForCaching
|
|
FIntRect NewCacheBounds(bIsValid ? FMath::Min(X1, CachedX1) : X1, bIsValid ? FMath::Min(Y1, CachedY1) : Y1, bIsValid ? FMath::Max(X2, CachedX2) : X2, bIsValid ? FMath::Max(Y2, CachedY2) : Y2);
|
|
|
|
// The caller might request a cache region that is actually larger than the data they want to sample for this particular read (e.g. to avoid re-caching when doing expensive samples like layer-collapsing ones) :
|
|
NewCacheBounds = PrepareRegionForCaching(NewCacheBounds);
|
|
check((NewCacheBounds.Min.X <= X1) && (NewCacheBounds.Min.Y <= Y1) && (NewCacheBounds.Max.X >= X2) && (NewCacheBounds.Max.Y >= Y2));
|
|
|
|
CacheData(NewCacheBounds.Min.X, NewCacheBounds.Min.Y, NewCacheBounds.Max.X, NewCacheBounds.Max.Y);
|
|
}
|
|
ensure(HasCachedData(X1, Y1, X2, Y2));
|
|
return GetCachedData(X1, Y1, X2, Y2, OutData);
|
|
}
|
|
|
|
// X2/Y2 Coordinates are "inclusive" max values
|
|
bool GetCachedData(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<AccessorType>& OutData)
|
|
{
|
|
const int32 XSize = (1 + X2 - X1);
|
|
const int32 YSize = (1 + Y2 - Y1);
|
|
const int32 NumSamples = XSize * YSize;
|
|
OutData.Empty(NumSamples);
|
|
OutData.AddUninitialized(NumSamples);
|
|
bool bHasNonZero = false;
|
|
|
|
for (int32 Y = Y1; Y <= Y2; Y++)
|
|
{
|
|
const int32 YOffset = (Y - Y1) * XSize;
|
|
for (int32 X = X1; X <= X2; X++)
|
|
{
|
|
const int32 XYOffset = YOffset + (X - X1);
|
|
AccessorType* Ptr = GetValueRef(X, Y);
|
|
if (Ptr)
|
|
{
|
|
OutData[XYOffset] = *Ptr;
|
|
if (!IsZeroValue(*Ptr))
|
|
{
|
|
bHasNonZero = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OutData[XYOffset] = (AccessorType)0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bHasNonZero;
|
|
}
|
|
|
|
// X2/Y2 Coordinates are "inclusive" max values
|
|
void SetCachedData(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<AccessorType>& Data, ELandscapeLayerPaintingRestriction PaintingRestriction = ELandscapeLayerPaintingRestriction::None, bool bUpdateData = true)
|
|
{
|
|
checkSlow(Data.Num() == (1 + Y2 - Y1) * (1 + X2 - X1));
|
|
|
|
// Update cache
|
|
for (int32 Y = Y1; Y <= Y2; Y++)
|
|
{
|
|
for (int32 X = X1; X <= X2; X++)
|
|
{
|
|
SetValue(X, Y, Data[(X - X1) + (Y - Y1)*(1 + X2 - X1)]);
|
|
}
|
|
}
|
|
|
|
if (bUpdateData)
|
|
{
|
|
// Update real data
|
|
DataAccess.SetData(X1, Y1, X2, Y2, Data.GetData(), PaintingRestriction);
|
|
}
|
|
}
|
|
|
|
// Get the original data before we made any changes with the SetCachedData interface.
|
|
// X2/Y2 Coordinates are "inclusive" max values
|
|
void GetOriginalData(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<AccessorType>& OutOriginalData)
|
|
{
|
|
int32 NumSamples = (1 + X2 - X1)*(1 + Y2 - Y1);
|
|
OutOriginalData.Empty(NumSamples);
|
|
OutOriginalData.AddUninitialized(NumSamples);
|
|
|
|
for (int32 Y = Y1; Y <= Y2; Y++)
|
|
{
|
|
for (int32 X = X1; X <= X2; X++)
|
|
{
|
|
AccessorType* Ptr = OriginalData.Find(FIntPoint(X, Y));
|
|
if (Ptr)
|
|
{
|
|
OutOriginalData[(X - X1) + (Y - Y1)*(1 + X2 - X1)] = *Ptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Flush()
|
|
{
|
|
DataAccess.Flush();
|
|
}
|
|
|
|
private:
|
|
// X2/Y2 Coordinates are "inclusive" max values
|
|
void CacheOriginalData(int32 X1, int32 Y1, int32 X2, int32 Y2)
|
|
{
|
|
for (int32 Y = Y1; Y <= Y2; Y++)
|
|
{
|
|
for (int32 X = X1; X <= X2; X++)
|
|
{
|
|
FIntPoint Key = FIntPoint(X, Y);
|
|
AccessorType* Ptr = CachedData.Find(Key);
|
|
if (Ptr)
|
|
{
|
|
check(OriginalData.Find(Key) == NULL);
|
|
OriginalData.Add(Key, *Ptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void VisualizeLandscapeRegion(int32 InX1, int32 InY1, int32 InX2, int32 InY2, const FColor& InColor, const FString& InDescription)
|
|
{
|
|
check(LandscapeInfo != nullptr);
|
|
const FTransform& LandscapeTransform = LandscapeInfo->LandscapeActor->GetTransform();
|
|
FVector Min = LandscapeTransform.TransformPosition(FVector(InX1, InY1, 0));
|
|
FVector Max = LandscapeTransform.TransformPosition(FVector(InX2, InY2, 0));
|
|
UE_VLOG_BOX(LandscapeInfo->LandscapeActor.Get(), LogLandscapeTools, Log, FBox(Min, Max), InColor, TEXT("%s"), *InDescription);
|
|
}
|
|
|
|
TMap<FIntPoint, AccessorType> CachedData;
|
|
TMap<FIntPoint, AccessorType> OriginalData;
|
|
// Keep the landscape info for visual logging purposes :
|
|
TWeakObjectPtr<ULandscapeInfo> LandscapeInfo;
|
|
|
|
bool bIsValid = false;
|
|
|
|
int32 CachedX1 = INDEX_NONE;
|
|
int32 CachedY1 = INDEX_NONE;
|
|
int32 CachedX2 = INDEX_NONE;
|
|
int32 CachedY2 = INDEX_NONE;
|
|
|
|
// To store valid region....
|
|
int32 ValidX1 = INDEX_NONE;
|
|
int32 ValidX2 = INDEX_NONE;
|
|
int32 ValidY1 = INDEX_NONE;
|
|
int32 ValidY2 = INDEX_NONE;
|
|
};
|
|
|
|
template<bool bInUseInterp>
|
|
struct FHeightmapAccessorTool : public FHeightmapAccessor<bInUseInterp>
|
|
{
|
|
FHeightmapAccessorTool(const FLandscapeToolTarget& InTarget)
|
|
: FHeightmapAccessor<bInUseInterp>(InTarget.LandscapeInfo.Get())
|
|
{
|
|
}
|
|
};
|
|
|
|
struct FLandscapeHeightCache : public TLandscapeEditCache<FHeightmapAccessorTool<true>, uint16>
|
|
{
|
|
static uint16 ClampValue(int32 Value) { return FMath::Clamp(Value, 0, LandscapeDataAccess::MaxValue); }
|
|
|
|
FLandscapeHeightCache(const FLandscapeToolTarget& InTarget)
|
|
: TLandscapeEditCache<FHeightmapAccessorTool<true>, uint16>(InTarget)
|
|
{
|
|
}
|
|
};
|
|
|
|
//
|
|
// FXYOffsetmapAccessor
|
|
//
|
|
template<bool bInUseInterp>
|
|
struct FXYOffsetmapAccessor
|
|
{
|
|
enum { bUseInterp = bInUseInterp };
|
|
FXYOffsetmapAccessor(ULandscapeInfo* InLandscapeInfo)
|
|
{
|
|
LandscapeInfo = InLandscapeInfo;
|
|
LandscapeEdit = new FLandscapeEditDataInterface(InLandscapeInfo);
|
|
}
|
|
|
|
FXYOffsetmapAccessor(const FLandscapeToolTarget& InTarget)
|
|
: FXYOffsetmapAccessor(InTarget.LandscapeInfo.Get())
|
|
{
|
|
}
|
|
|
|
// accessors
|
|
void GetData(int32& X1, int32& Y1, int32& X2, int32& Y2, TMap<FIntPoint, FVector>& Data)
|
|
{
|
|
LandscapeEdit->GetXYOffsetData(X1, Y1, X2, Y2, Data);
|
|
|
|
TMap<FIntPoint, uint16> NewHeights;
|
|
LandscapeEdit->GetHeightData(X1, Y1, X2, Y2, NewHeights);
|
|
for (int32 Y = Y1; Y <= Y2; ++Y)
|
|
{
|
|
for (int32 X = X1; X <= X2; ++X)
|
|
{
|
|
FVector* Value = Data.Find(FIntPoint(X, Y));
|
|
if (Value)
|
|
{
|
|
Value->Z = ((float)NewHeights.FindRef(FIntPoint(X, Y)) - 32768.0f) * LANDSCAPE_ZSCALE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GetDataFast(int32 X1, int32 Y1, int32 X2, int32 Y2, TMap<FIntPoint, FVector>& Data)
|
|
{
|
|
LandscapeEdit->GetXYOffsetDataFast(X1, Y1, X2, Y2, Data);
|
|
|
|
TMap<FIntPoint, uint16> NewHeights;
|
|
LandscapeEdit->GetHeightData(X1, Y1, X2, Y2, NewHeights);
|
|
for (int32 Y = Y1; Y <= Y2; ++Y)
|
|
{
|
|
for (int32 X = X1; X <= X2; ++X)
|
|
{
|
|
FVector* Value = Data.Find(FIntPoint(X, Y));
|
|
if (Value)
|
|
{
|
|
Value->Z = ((float)NewHeights.FindRef(FIntPoint(X, Y)) - 32768.0f) * LANDSCAPE_ZSCALE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetData(int32 X1, int32 Y1, int32 X2, int32 Y2, const FVector* Data, ELandscapeLayerPaintingRestriction PaintingRestriction = ELandscapeLayerPaintingRestriction::None)
|
|
{
|
|
TSet<ULandscapeComponent*> Components;
|
|
if (LandscapeInfo && LandscapeEdit->GetComponentsInRegion(X1, Y1, X2, Y2, &Components))
|
|
{
|
|
// Update data
|
|
ChangedComponents.Append(Components);
|
|
|
|
// Convert Height to uint16
|
|
TArray<uint16> NewHeights;
|
|
NewHeights.AddZeroed((Y2 - Y1 + 1) * (X2 - X1 + 1));
|
|
for (int32 Y = Y1; Y <= Y2; ++Y)
|
|
{
|
|
for (int32 X = X1; X <= X2; ++X)
|
|
{
|
|
NewHeights[X - X1 + (Y - Y1) * (X2 - X1 + 1)] = FMath::Clamp<uint16>(Data[(X - X1 + (Y - Y1) * (X2 - X1 + 1))].Z * LANDSCAPE_INV_ZSCALE + 32768.0f, 0, 65535);
|
|
}
|
|
}
|
|
|
|
// Notify foliage to move any attached instances
|
|
bool bUpdateFoliage = false;
|
|
bool bUpdateNormals = false;
|
|
|
|
if (!LandscapeEdit->HasLandscapeLayersContent())
|
|
{
|
|
ALandscapeProxy::InvalidateGeneratedComponentData(Components);
|
|
|
|
bUpdateNormals = true;
|
|
for (ULandscapeComponent* Component : Components)
|
|
{
|
|
ULandscapeHeightfieldCollisionComponent* CollisionComponent = Component->CollisionComponent.Get();
|
|
if (CollisionComponent && AInstancedFoliageActor::HasFoliageAttached(CollisionComponent))
|
|
{
|
|
bUpdateFoliage = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bUpdateFoliage)
|
|
{
|
|
// Calculate landscape local-space bounding box of old data, to look for foliage instances.
|
|
TArray<ULandscapeHeightfieldCollisionComponent*> CollisionComponents;
|
|
CollisionComponents.Empty(Components.Num());
|
|
TArray<FBox> PreUpdateLocalBoxes;
|
|
PreUpdateLocalBoxes.Empty(Components.Num());
|
|
|
|
for (ULandscapeComponent* Component : Components)
|
|
{
|
|
CollisionComponents.Add(Component->CollisionComponent.Get());
|
|
PreUpdateLocalBoxes.Add(FBox(FVector((float)X1, (float)Y1, Component->CachedLocalBox.Min.Z), FVector((float)X2, (float)Y2, Component->CachedLocalBox.Max.Z)));
|
|
}
|
|
|
|
// Update landscape.
|
|
LandscapeEdit->SetXYOffsetData(X1, Y1, X2, Y2, Data, 0); // XY Offset always need to be update before the height update
|
|
LandscapeEdit->SetHeightData(X1, Y1, X2, Y2, NewHeights.GetData(), 0, bUpdateNormals);
|
|
|
|
// Snap foliage for each component.
|
|
for (int32 Index = 0; Index < CollisionComponents.Num(); ++Index)
|
|
{
|
|
ULandscapeHeightfieldCollisionComponent* CollisionComponent = CollisionComponents[Index];
|
|
CollisionComponent->SnapFoliageInstances(PreUpdateLocalBoxes[Index].TransformBy(LandscapeInfo->GetLandscapeProxy()->LandscapeActorToWorld().ToMatrixWithScale()).ExpandBy(1.0f));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No foliage, just update landscape.
|
|
LandscapeEdit->SetXYOffsetData(X1, Y1, X2, Y2, Data, 0); // XY Offset always need to be update before the height update
|
|
LandscapeEdit->SetHeightData(X1, Y1, X2, Y2, NewHeights.GetData(), 0, bUpdateNormals);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Flush()
|
|
{
|
|
LandscapeEdit->Flush();
|
|
}
|
|
|
|
virtual ~FXYOffsetmapAccessor()
|
|
{
|
|
delete LandscapeEdit;
|
|
LandscapeEdit = NULL;
|
|
|
|
// Update the bounds for the components we edited
|
|
for (TSet<ULandscapeComponent*>::TConstIterator It(ChangedComponents); It; ++It)
|
|
{
|
|
(*It)->UpdateCachedBounds();
|
|
(*It)->UpdateComponentToWorld();
|
|
}
|
|
}
|
|
|
|
private:
|
|
ULandscapeInfo* LandscapeInfo;
|
|
FLandscapeEditDataInterface* LandscapeEdit;
|
|
TSet<ULandscapeComponent*> ChangedComponents;
|
|
};
|
|
|
|
template<bool bInUseInterp>
|
|
struct FLandscapeXYOffsetCache : public TLandscapeEditCache<FXYOffsetmapAccessor<bInUseInterp>, FVector>
|
|
{
|
|
FLandscapeXYOffsetCache(const FLandscapeToolTarget& InTarget)
|
|
: TLandscapeEditCache<FXYOffsetmapAccessor<bInUseInterp>, FVector>(InTarget)
|
|
{
|
|
}
|
|
};
|
|
|
|
template<bool bInUseInterp, bool bInUseTotalNormalize>
|
|
struct FAlphamapAccessorTool : public FAlphamapAccessor<bInUseInterp, bInUseTotalNormalize>
|
|
{
|
|
FAlphamapAccessorTool(ULandscapeInfo* InLandscapeInfo, ULandscapeLayerInfoObject* InLayerInfo)
|
|
: FAlphamapAccessor<bInUseInterp, bInUseTotalNormalize>(InLandscapeInfo, InLayerInfo)
|
|
{}
|
|
|
|
FAlphamapAccessorTool(const FLandscapeToolTarget& InTarget)
|
|
: FAlphamapAccessor<bInUseInterp, bInUseTotalNormalize>(InTarget.LandscapeInfo.Get(), InTarget.LayerInfo.Get())
|
|
{
|
|
}
|
|
};
|
|
|
|
struct FLandscapeAlphaCache : public TLandscapeEditCache<FAlphamapAccessorTool<true, false>, uint8>
|
|
{
|
|
static uint8 ClampValue(int32 Value) { return FMath::Clamp(Value, 0, 255); }
|
|
|
|
FLandscapeAlphaCache(const FLandscapeToolTarget& InTarget)
|
|
: TLandscapeEditCache<FAlphamapAccessorTool<true, false>, uint8>(InTarget)
|
|
{
|
|
}
|
|
};
|
|
|
|
struct FVisibilityAccessor : public FAlphamapAccessorTool<false, false>
|
|
{
|
|
FVisibilityAccessor(const FLandscapeToolTarget& InTarget)
|
|
: FAlphamapAccessorTool<false, false>(InTarget.LandscapeInfo.Get(), ALandscapeProxy::VisibilityLayer)
|
|
{
|
|
}
|
|
};
|
|
|
|
struct FLandscapeVisCache : public TLandscapeEditCache<FAlphamapAccessorTool<false, false>, uint8>
|
|
{
|
|
static uint8 ClampValue(int32 Value) { return FMath::Clamp(Value, 0, 255); }
|
|
|
|
FLandscapeVisCache(const FLandscapeToolTarget& InTarget)
|
|
: TLandscapeEditCache<FAlphamapAccessorTool<false, false>, uint8>(InTarget)
|
|
{
|
|
}
|
|
};
|
|
|
|
|
|
template<class ToolTarget>
|
|
class FLandscapeLayerDataCache
|
|
{
|
|
public:
|
|
FLandscapeLayerDataCache(const FLandscapeToolTarget& InTarget, typename ToolTarget::CacheClass& Cache)
|
|
: LandscapeInfo(nullptr)
|
|
, Landscape(nullptr)
|
|
, EditingLayerIndex(MAX_uint8)
|
|
, bIsInitialized(false)
|
|
, bCombinedLayerOperation(false)
|
|
, bVisibilityChanged(false)
|
|
, bTargetIsHeightmap(InTarget.TargetType == ELandscapeToolTargetType::Heightmap)
|
|
, CacheUpToEditingLayer(Cache)
|
|
, CacheBottomLayers(InTarget)
|
|
{
|
|
}
|
|
|
|
void Initialize(ULandscapeInfo* InLandscapeInfo, bool InCombinedLayerOperation)
|
|
{
|
|
if (!bIsInitialized)
|
|
{
|
|
LandscapeInfo = InLandscapeInfo;
|
|
Landscape = LandscapeInfo ? LandscapeInfo->LandscapeActor.Get() : nullptr;
|
|
bCombinedLayerOperation = Landscape && Landscape->HasLayersContent() && InCombinedLayerOperation && bTargetIsHeightmap;
|
|
if (bCombinedLayerOperation)
|
|
{
|
|
EditingLayerGuid = Landscape->GetEditingLayer();
|
|
for (int i = 0; i < Landscape->GetLayerCount(); ++i)
|
|
{
|
|
FLandscapeLayer* CurrentLayer = Landscape->GetLayer(i);
|
|
BackupLayerVisibility.Add(CurrentLayer->bVisible);
|
|
if (CurrentLayer->Guid == EditingLayerGuid)
|
|
{
|
|
EditingLayerIndex = i;
|
|
}
|
|
}
|
|
check(EditingLayerIndex < Landscape->GetLayerCount());
|
|
}
|
|
bIsInitialized = true;
|
|
}
|
|
}
|
|
|
|
void Read(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<typename ToolTarget::CacheClass::DataType>& Data)
|
|
{
|
|
check(bIsInitialized);
|
|
if (bCombinedLayerOperation)
|
|
{
|
|
TArray<bool> NewLayerVisibility;
|
|
for (int i = 0; i < Landscape->GetLayerCount(); ++i)
|
|
{
|
|
FLandscapeLayer* CurrentLayer = Landscape->GetLayer(i);
|
|
NewLayerVisibility.Add((i > EditingLayerIndex) ? false : CurrentLayer->bVisible);
|
|
}
|
|
|
|
auto OnCacheUpdating = [&](const FIntRect& NewCacheBounds) -> FIntRect
|
|
{
|
|
// This function is triggered when the cache needs expanding. We'll ask for a larger area so that we don't need to re-cache (a GPU-synchronizing operation) every time
|
|
// we need an additional row/column. Aligning the cache region on components borders is appropriate since this is what gets rendered. This avoids
|
|
// re-caching when we've already sampled a component:
|
|
FIntRect DesiredCacheBounds;
|
|
DesiredCacheBounds.Min.X = (FMath::FloorToInt((float)NewCacheBounds.Min.X / LandscapeInfo->ComponentSizeQuads)) * LandscapeInfo->ComponentSizeQuads;
|
|
DesiredCacheBounds.Min.Y = (FMath::FloorToInt((float)NewCacheBounds.Min.Y / LandscapeInfo->ComponentSizeQuads)) * LandscapeInfo->ComponentSizeQuads;
|
|
DesiredCacheBounds.Max.X = (FMath::CeilToInt((float)NewCacheBounds.Max.X / LandscapeInfo->ComponentSizeQuads)) * LandscapeInfo->ComponentSizeQuads;
|
|
DesiredCacheBounds.Max.Y = (FMath::CeilToInt((float)NewCacheBounds.Max.Y / LandscapeInfo->ComponentSizeQuads)) * LandscapeInfo->ComponentSizeQuads;
|
|
|
|
TSet<ULandscapeComponent*> AffectedComponents;
|
|
LandscapeInfo->GetComponentsInRegion(DesiredCacheBounds.Min.X, DesiredCacheBounds.Min.Y, DesiredCacheBounds.Max.X, DesiredCacheBounds.Max.Y, AffectedComponents);
|
|
SynchronousUpdateComponentVisibilityForHeight(AffectedComponents, NewLayerVisibility);
|
|
|
|
if (bVisibilityChanged)
|
|
{
|
|
VisibilityChangedComponents.Append(AffectedComponents);
|
|
}
|
|
|
|
return DesiredCacheBounds;
|
|
};
|
|
|
|
FScopedSetLandscapeEditingLayer Scope(Landscape, FGuid());
|
|
CacheUpToEditingLayer.GetDataAndCache(X1, Y1, X2, Y2, Data, OnCacheUpdating);
|
|
// Release Texture Mips that will be Locked by the next SynchronousUpdateComponentVisibilityForHeight
|
|
CacheUpToEditingLayer.DataAccess.Flush();
|
|
|
|
// Now turn off visibility on the current layer in order to have the data of all bottom layers except the current one
|
|
NewLayerVisibility[EditingLayerIndex] = false;
|
|
CacheBottomLayers.GetDataAndCache(X1, Y1, X2, Y2, BottomLayersData, OnCacheUpdating);
|
|
// Do the same here for consistency
|
|
CacheBottomLayers.DataAccess.Flush();
|
|
}
|
|
else
|
|
{
|
|
CacheUpToEditingLayer.CacheData(X1, Y1, X2, Y2);
|
|
CacheUpToEditingLayer.GetCachedData(X1, Y1, X2, Y2, Data);
|
|
}
|
|
}
|
|
|
|
void Write(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<typename ToolTarget::CacheClass::DataType>& Data, ELandscapeLayerPaintingRestriction PaintingRestriction = ELandscapeLayerPaintingRestriction::None)
|
|
{
|
|
check(bIsInitialized);
|
|
if (bCombinedLayerOperation)
|
|
{
|
|
const float Alpha = Landscape->GetLayerAlpha(EditingLayerIndex, bTargetIsHeightmap);
|
|
const float InverseAlpha = (Alpha != 0.f) ? 1.f / Alpha : 1.f;
|
|
TArray<typename ToolTarget::CacheClass::DataType> DataContribution;
|
|
DataContribution.Empty(Data.Num());
|
|
DataContribution.AddUninitialized(Data.Num());
|
|
check(Data.Num() == BottomLayersData.Num());
|
|
for (int i = 0; i < Data.Num(); ++i)
|
|
{
|
|
float Contribution = (LandscapeDataAccess::GetLocalHeight(Data[i]) - LandscapeDataAccess::GetLocalHeight(BottomLayersData[i])) * InverseAlpha;
|
|
DataContribution[i] = LandscapeDataAccess::GetTexHeight(Contribution);
|
|
}
|
|
checkf(EditingLayerGuid == Landscape->GetEditingLayer(), TEXT("EditingLayer has changed between Initialize and Write. Was: %s (%s). Is now: %s (%s)"),
|
|
Landscape->GetLayer(EditingLayerGuid) ? *(Landscape->GetLayer(EditingLayerGuid)->Name.ToString()) : TEXT("<unknown>"), *EditingLayerGuid.ToString(),
|
|
Landscape->GetLayer(Landscape->GetEditingLayer()) ? *(Landscape->GetLayer(Landscape->GetEditingLayer())->Name.ToString()) : TEXT("<unknown>"), *Landscape->GetEditingLayer().ToString());
|
|
|
|
// Restore layers visibility
|
|
SetLayersVisibility(BackupLayerVisibility);
|
|
// Only store data in cache
|
|
CacheUpToEditingLayer.SetCachedData(X1, Y1, X2, Y2, Data, PaintingRestriction, false);
|
|
// Effectively write the contribution
|
|
CacheUpToEditingLayer.DataAccess.SetData(X1, Y1, X2, Y2, DataContribution.GetData(), PaintingRestriction);
|
|
CacheUpToEditingLayer.DataAccess.Flush();
|
|
if (bVisibilityChanged)
|
|
{
|
|
const bool bUpdateCollision = true;
|
|
const bool bIntermediateRender = false;
|
|
SynchronousUpdateHeightmapForComponents(VisibilityChangedComponents, bUpdateCollision, bIntermediateRender);
|
|
VisibilityChangedComponents.Empty();
|
|
bVisibilityChanged = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CacheUpToEditingLayer.SetCachedData(X1, Y1, X2, Y2, Data, PaintingRestriction);
|
|
CacheUpToEditingLayer.Flush();
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
void SynchronousUpdateHeightmapForComponents(const TSet<ULandscapeComponent*>& InComponents, bool bUpdateCollision, bool bIntermediateRender)
|
|
{
|
|
for (ULandscapeComponent* Component : InComponents)
|
|
{
|
|
const bool bUpdateAll = false; // default value
|
|
Component->RequestHeightmapUpdate(bUpdateAll, bUpdateCollision);
|
|
}
|
|
Landscape->ForceUpdateLayersContent(bIntermediateRender);
|
|
};
|
|
|
|
void SetLayersVisibility(const TArray<bool>& InLayerVisibility)
|
|
{
|
|
check(InLayerVisibility.Num() == Landscape->GetLayerCount());
|
|
for (int i = 0; i < InLayerVisibility.Num(); ++i)
|
|
{
|
|
if (FLandscapeLayer* Layer = Landscape->GetLayer(i))
|
|
{
|
|
if (Layer->bVisible != InLayerVisibility[i])
|
|
{
|
|
Layer->bVisible = InLayerVisibility[i];
|
|
bVisibilityChanged = true;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
void SynchronousUpdateComponentVisibilityForHeight(const TSet<ULandscapeComponent*>& InComponents, const TArray<bool>& InLayerVisibility)
|
|
{
|
|
SetLayersVisibility(InLayerVisibility);
|
|
// No need to update collision here as we are only doing a intermediate render to gather heightdata
|
|
const bool bUpdateCollision = false;
|
|
const bool bIntermediateRender = true;
|
|
SynchronousUpdateHeightmapForComponents(InComponents, bUpdateCollision, bIntermediateRender);
|
|
};
|
|
|
|
ULandscapeInfo* LandscapeInfo;
|
|
ALandscape* Landscape;
|
|
FGuid EditingLayerGuid;
|
|
uint8 EditingLayerIndex;
|
|
TArray<bool> BackupLayerVisibility;
|
|
TArray<typename ToolTarget::CacheClass::DataType> BottomLayersData;
|
|
bool bIsInitialized;
|
|
bool bCombinedLayerOperation;
|
|
bool bVisibilityChanged;
|
|
bool bTargetIsHeightmap;
|
|
TSet<ULandscapeComponent*> VisibilityChangedComponents;
|
|
|
|
typename ToolTarget::CacheClass& CacheUpToEditingLayer;
|
|
typename ToolTarget::CacheClass CacheBottomLayers;
|
|
};
|
|
|
|
//
|
|
// FFullWeightmapAccessor
|
|
//
|
|
template<bool bInUseInterp>
|
|
struct FFullWeightmapAccessor
|
|
{
|
|
enum { bUseInterp = bInUseInterp };
|
|
FFullWeightmapAccessor(ULandscapeInfo* InLandscapeInfo)
|
|
: LandscapeInfo(InLandscapeInfo)
|
|
, LandscapeEdit(InLandscapeInfo)
|
|
{
|
|
}
|
|
|
|
FFullWeightmapAccessor(const FLandscapeToolTarget& InTarget)
|
|
: FFullWeightmapAccessor(InTarget.LandscapeInfo.Get())
|
|
{
|
|
}
|
|
|
|
~FFullWeightmapAccessor()
|
|
{
|
|
if (!LandscapeEdit.HasLandscapeLayersContent())
|
|
{
|
|
// Recreate collision for modified components to update the physical materials
|
|
for (ULandscapeComponent* Component : ModifiedComponents)
|
|
{
|
|
ULandscapeHeightfieldCollisionComponent* CollisionComponent = Component->CollisionComponent.Get();
|
|
if (CollisionComponent)
|
|
{
|
|
CollisionComponent->RecreateCollision();
|
|
|
|
// We need to trigger navigation mesh build, in case user have painted holes on a landscape
|
|
if (LandscapeInfo->GetLayerInfoIndex(ALandscapeProxy::VisibilityLayer) != INDEX_NONE)
|
|
{
|
|
FNavigationSystem::UpdateComponentData(*CollisionComponent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GetData(int32& X1, int32& Y1, int32& X2, int32& Y2, TMap<FIntPoint, TArray<uint8>>& Data)
|
|
{
|
|
// Do not Support for interpolation....
|
|
check(false && TEXT("Do not support interpolation for FullWeightmapAccessor for now"));
|
|
}
|
|
|
|
void GetDataFast(int32 X1, int32 Y1, int32 X2, int32 Y2, TMap<FIntPoint, TArray<uint8>>& Data)
|
|
{
|
|
DirtyLayerInfos.Empty();
|
|
LandscapeEdit.GetWeightDataFast(NULL, X1, Y1, X2, Y2, Data);
|
|
}
|
|
|
|
void SetData(int32 X1, int32 Y1, int32 X2, int32 Y2, const uint8* Data, ELandscapeLayerPaintingRestriction PaintingRestriction)
|
|
{
|
|
TSet<ULandscapeComponent*> Components;
|
|
if (LandscapeEdit.GetComponentsInRegion(X1, Y1, X2, Y2, &Components))
|
|
{
|
|
if (!LandscapeEdit.HasLandscapeLayersContent())
|
|
{
|
|
ALandscapeProxy::InvalidateGeneratedComponentData(Components);
|
|
ModifiedComponents.Append(Components);
|
|
}
|
|
LandscapeEdit.SetAlphaData(DirtyLayerInfos, X1, Y1, X2, Y2, Data, 0, PaintingRestriction);
|
|
}
|
|
DirtyLayerInfos.Empty();
|
|
}
|
|
|
|
void Flush()
|
|
{
|
|
LandscapeEdit.Flush();
|
|
}
|
|
|
|
TSet<ULandscapeLayerInfoObject*> DirtyLayerInfos;
|
|
|
|
private:
|
|
ULandscapeInfo* LandscapeInfo;
|
|
FLandscapeEditDataInterface LandscapeEdit;
|
|
TSet<ULandscapeComponent*> ModifiedComponents;
|
|
};
|
|
|
|
struct FLandscapeFullWeightCache : public TLandscapeEditCache<FFullWeightmapAccessor<false>, TArray<uint8>>
|
|
{
|
|
FLandscapeFullWeightCache(const FLandscapeToolTarget& InTarget)
|
|
: TLandscapeEditCache<FFullWeightmapAccessor<false>, TArray<uint8>>(InTarget)
|
|
{
|
|
}
|
|
|
|
// Only for all weight case... the accessor type should be TArray<uint8>
|
|
void GetCachedData(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<uint8>& OutData, int32 ArraySize)
|
|
{
|
|
if (ArraySize == 0)
|
|
{
|
|
OutData.Empty();
|
|
return;
|
|
}
|
|
|
|
const int32 XSize = (1 + X2 - X1);
|
|
const int32 YSize = (1 + Y2 - Y1);
|
|
const int32 Stride = XSize * ArraySize;
|
|
int32 NumSamples = XSize * YSize * ArraySize;
|
|
OutData.Empty(NumSamples);
|
|
OutData.AddUninitialized(NumSamples);
|
|
|
|
for (int32 Y = Y1; Y <= Y2; Y++)
|
|
{
|
|
const int32 YOffset = (Y - Y1) * Stride;
|
|
for (int32 X = X1; X <= X2; X++)
|
|
{
|
|
const int32 XYOffset = YOffset + (X - X1) * ArraySize;
|
|
TArray<uint8>* Ptr = GetValueRef(X, Y);
|
|
if (Ptr)
|
|
{
|
|
for (int32 Z = 0; Z < ArraySize; Z++)
|
|
{
|
|
OutData[XYOffset + Z] = (*Ptr)[Z];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FMemory::Memzero((void*)&OutData[XYOffset], (SIZE_T)ArraySize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only for all weight case... the accessor type should be TArray<uint8>
|
|
void SetCachedData(int32 X1, int32 Y1, int32 X2, int32 Y2, TArray<uint8>& Data, int32 ArraySize, ELandscapeLayerPaintingRestriction PaintingRestriction)
|
|
{
|
|
// Update cache
|
|
for (int32 Y = Y1; Y <= Y2; Y++)
|
|
{
|
|
for (int32 X = X1; X <= X2; X++)
|
|
{
|
|
TArray<uint8> Value;
|
|
Value.Empty(ArraySize);
|
|
Value.AddUninitialized(ArraySize);
|
|
for (int32 Z = 0; Z < ArraySize; Z++)
|
|
{
|
|
Value[Z] = Data[((X - X1) + (Y - Y1)*(1 + X2 - X1)) * ArraySize + Z];
|
|
}
|
|
SetValue(X, Y, MoveTemp(Value));
|
|
}
|
|
}
|
|
|
|
// Update real data
|
|
DataAccess.SetData(X1, Y1, X2, Y2, Data.GetData(), PaintingRestriction);
|
|
}
|
|
|
|
void AddDirtyLayer(ULandscapeLayerInfoObject* LayerInfo)
|
|
{
|
|
DataAccess.DirtyLayerInfos.Add(LayerInfo);
|
|
}
|
|
};
|
|
|
|
//
|
|
// FDatamapAccessor
|
|
//
|
|
template<bool bInUseInterp>
|
|
struct FDatamapAccessor
|
|
{
|
|
enum { bUseInterp = bInUseInterp };
|
|
FDatamapAccessor(ULandscapeInfo* InLandscapeInfo)
|
|
: LandscapeEdit(InLandscapeInfo)
|
|
{
|
|
}
|
|
|
|
FDatamapAccessor(const FLandscapeToolTarget& InTarget)
|
|
: FDatamapAccessor(InTarget.LandscapeInfo.Get())
|
|
{
|
|
}
|
|
|
|
void GetData(int32& X1, int32& Y1, int32& X2, int32& Y2, TMap<FIntPoint, uint8>& Data)
|
|
{
|
|
LandscapeEdit.GetSelectData(X1, Y1, X2, Y2, Data);
|
|
}
|
|
|
|
void GetDataFast(const int32 X1, const int32 Y1, const int32 X2, const int32 Y2, TMap<FIntPoint, uint8>& Data)
|
|
{
|
|
LandscapeEdit.GetSelectData(X1, Y1, X2, Y2, Data);
|
|
}
|
|
|
|
void SetData(int32 X1, int32 Y1, int32 X2, int32 Y2, const uint8* Data, ELandscapeLayerPaintingRestriction PaintingRestriction = ELandscapeLayerPaintingRestriction::None)
|
|
{
|
|
if (LandscapeEdit.GetComponentsInRegion(X1, Y1, X2, Y2))
|
|
{
|
|
LandscapeEdit.SetSelectData(X1, Y1, X2, Y2, Data, 0);
|
|
}
|
|
}
|
|
|
|
void Flush()
|
|
{
|
|
LandscapeEdit.Flush();
|
|
}
|
|
|
|
private:
|
|
FLandscapeEditDataInterface LandscapeEdit;
|
|
};
|
|
|
|
struct FLandscapeDataCache : public TLandscapeEditCache<FDatamapAccessor<false>, uint8>
|
|
{
|
|
static uint8 ClampValue(int32 Value) { return FMath::Clamp(Value, 0, 255); }
|
|
|
|
FLandscapeDataCache(const FLandscapeToolTarget& InTarget)
|
|
: TLandscapeEditCache<FDatamapAccessor<false>, uint8>(InTarget)
|
|
{
|
|
}
|
|
};
|
|
|
|
|
|
//
|
|
// Tool targets
|
|
//
|
|
struct FHeightmapToolTarget
|
|
{
|
|
typedef FLandscapeHeightCache CacheClass;
|
|
static const ELandscapeToolTargetType::Type TargetType = ELandscapeToolTargetType::Heightmap;
|
|
|
|
static float StrengthMultiplier(ULandscapeInfo* LandscapeInfo, float BrushRadius)
|
|
{
|
|
if (LandscapeInfo)
|
|
{
|
|
// Adjust strength based on brush size and drawscale, so strength 1 = one hemisphere
|
|
return BrushRadius * LANDSCAPE_INV_ZSCALE / (LandscapeInfo->DrawScale.Z);
|
|
}
|
|
return 5.0f * LANDSCAPE_INV_ZSCALE;
|
|
}
|
|
|
|
static FMatrix ToWorldMatrix(ULandscapeInfo* LandscapeInfo)
|
|
{
|
|
FMatrix Result = FTranslationMatrix(FVector(0, 0, -32768.0f));
|
|
Result *= FScaleMatrix(FVector(1.0f, 1.0f, LANDSCAPE_ZSCALE) * LandscapeInfo->DrawScale);
|
|
return Result;
|
|
}
|
|
|
|
static FMatrix FromWorldMatrix(ULandscapeInfo* LandscapeInfo)
|
|
{
|
|
FMatrix Result = FScaleMatrix(FVector(1.0f, 1.0f, LANDSCAPE_INV_ZSCALE) / (LandscapeInfo->DrawScale));
|
|
Result *= FTranslationMatrix(FVector(0, 0, 32768.0f));
|
|
return Result;
|
|
}
|
|
};
|
|
|
|
|
|
struct FWeightmapToolTarget
|
|
{
|
|
typedef FLandscapeAlphaCache CacheClass;
|
|
static const ELandscapeToolTargetType::Type TargetType = ELandscapeToolTargetType::Weightmap;
|
|
|
|
static float StrengthMultiplier(ULandscapeInfo* LandscapeInfo, float BrushRadius)
|
|
{
|
|
return 255.0f;
|
|
}
|
|
|
|
static FMatrix ToWorldMatrix(ULandscapeInfo* LandscapeInfo) { return FMatrix::Identity; }
|
|
static FMatrix FromWorldMatrix(ULandscapeInfo* LandscapeInfo) { return FMatrix::Identity; }
|
|
};
|
|
|
|
/**
|
|
* FLandscapeToolStrokeBase - base class for tool strokes (used by FLandscapeToolBase)
|
|
*/
|
|
|
|
class FLandscapeToolStrokeBase : protected FGCObject
|
|
{
|
|
public:
|
|
// Whether to call Apply() every frame even if the mouse hasn't moved
|
|
enum { UseContinuousApply = false };
|
|
|
|
// This is also the expected signature of derived class constructor used by FLandscapeToolBase
|
|
FLandscapeToolStrokeBase(FEdModeLandscape* InEdMode, FEditorViewportClient* InViewportClient, const FLandscapeToolTarget& InTarget)
|
|
: EdMode(InEdMode)
|
|
, Target(InTarget)
|
|
, LandscapeInfo(InTarget.LandscapeInfo.Get())
|
|
{
|
|
}
|
|
|
|
// Signature of Apply() method for derived strokes
|
|
// void Apply(FEditorViewportClient* ViewportClient, FLandscapeBrush* Brush, const ULandscapeEditorObject* UISettings, const TArray<FLandscapeToolMousePosition>& MousePositions);
|
|
|
|
virtual void AddReferencedObjects(FReferenceCollector& Collector) override
|
|
{
|
|
Collector.AddReferencedObject(LandscapeInfo);
|
|
}
|
|
virtual FString GetReferencerName() const override
|
|
{
|
|
return TEXT("FLandscapeToolStrokeBase");
|
|
}
|
|
|
|
protected:
|
|
FEdModeLandscape* EdMode = nullptr;
|
|
const FLandscapeToolTarget& Target;
|
|
ULandscapeInfo* LandscapeInfo = nullptr;
|
|
};
|
|
|
|
|
|
/**
|
|
* FLandscapeToolBase - base class for painting tools
|
|
* ToolTarget - the target for the tool (weight or heightmap)
|
|
* StrokeClass - the class that implements the behavior for a mouse stroke applying the tool.
|
|
*/
|
|
template<class TStrokeClass>
|
|
class FLandscapeToolBase : public FLandscapeTool
|
|
{
|
|
public:
|
|
FLandscapeToolBase(FEdModeLandscape* InEdMode)
|
|
: LastInteractorPosition(FVector2D::ZeroVector)
|
|
, TimeSinceLastInteractorMove(0.0f)
|
|
, EdMode(InEdMode)
|
|
, bCanToolBeActivated(true)
|
|
, ToolStroke()
|
|
{
|
|
}
|
|
|
|
virtual bool ShouldUpdateEditingLayer() const
|
|
{
|
|
return AffectsEditLayers() && EdMode->CanHaveLandscapeLayersContent();
|
|
}
|
|
|
|
virtual ELandscapeLayerUpdateMode GetBeginToolContentUpdateFlag() const
|
|
{
|
|
bool bUpdateHeightmap = this->EdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Type::Heightmap;
|
|
return bUpdateHeightmap ? ELandscapeLayerUpdateMode::Update_Heightmap_Editing : ELandscapeLayerUpdateMode::Update_Weightmap_Editing;
|
|
}
|
|
|
|
virtual ELandscapeLayerUpdateMode GetTickToolContentUpdateFlag() const
|
|
{
|
|
return GetBeginToolContentUpdateFlag();
|
|
}
|
|
|
|
virtual ELandscapeLayerUpdateMode GetEndToolContentUpdateFlag() const
|
|
{
|
|
bool bUpdateHeightmap = this->EdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Type::Heightmap;
|
|
return bUpdateHeightmap ? ELandscapeLayerUpdateMode::Update_Heightmap_All : ELandscapeLayerUpdateMode::Update_Weightmap_All;
|
|
}
|
|
|
|
virtual bool BeginTool(FEditorViewportClient* ViewportClient, const FLandscapeToolTarget& InTarget, const FVector& InHitLocation) override
|
|
{
|
|
if (ShouldUpdateEditingLayer())
|
|
{
|
|
ALandscape* Landscape = this->EdMode->GetLandscape();
|
|
if (Landscape)
|
|
{
|
|
Landscape->RequestLayersContentUpdate(GetBeginToolContentUpdateFlag());
|
|
Landscape->SetEditingLayer(this->EdMode->GetCurrentLayerGuid());
|
|
Landscape->SetGrassUpdateEnabled(false);
|
|
}
|
|
}
|
|
|
|
if (!ensure(InteractorPositions.Num() == 0))
|
|
{
|
|
InteractorPositions.Empty(1);
|
|
}
|
|
|
|
if( !IsToolActive() )
|
|
{
|
|
ToolStroke.Emplace( EdMode, ViewportClient, InTarget );
|
|
EdMode->CurrentBrush->BeginStroke( InHitLocation.X, InHitLocation.Y, this );
|
|
}
|
|
|
|
// Save the mouse position
|
|
LastInteractorPosition = FVector2D(InHitLocation);
|
|
InteractorPositions.Emplace(LastInteractorPosition, ViewportClient ? IsModifierPressed(ViewportClient) : false); // Copy tool sometimes activates without a specific viewport via ctrl+c hotkey
|
|
TimeSinceLastInteractorMove = 0.0f;
|
|
|
|
ToolStroke->Apply(ViewportClient, EdMode->CurrentBrush, EdMode->UISettings, InteractorPositions);
|
|
|
|
InteractorPositions.Empty(1);
|
|
return true;
|
|
}
|
|
|
|
virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override
|
|
{
|
|
if (IsToolActive())
|
|
{
|
|
if (InteractorPositions.Num() > 0)
|
|
{
|
|
ToolStroke->Apply(ViewportClient, EdMode->CurrentBrush, EdMode->UISettings, InteractorPositions);
|
|
ViewportClient->Invalidate(false, false);
|
|
InteractorPositions.Empty(1);
|
|
}
|
|
else if (TStrokeClass::UseContinuousApply && TimeSinceLastInteractorMove >= 0.25f)
|
|
{
|
|
InteractorPositions.Emplace(LastInteractorPosition, IsModifierPressed(ViewportClient));
|
|
ToolStroke->Apply(ViewportClient, EdMode->CurrentBrush, EdMode->UISettings, InteractorPositions);
|
|
ViewportClient->Invalidate(false, false);
|
|
InteractorPositions.Empty(1);
|
|
}
|
|
TimeSinceLastInteractorMove += DeltaTime;
|
|
|
|
// Prevent landscape from baking textures while tool stroke is active
|
|
EdMode->CurrentToolTarget.LandscapeInfo->PostponeTextureBaking();
|
|
|
|
if (ShouldUpdateEditingLayer())
|
|
{
|
|
ALandscape* Landscape = this->EdMode->CurrentToolTarget.LandscapeInfo->LandscapeActor.Get();
|
|
if (Landscape != nullptr)
|
|
{
|
|
Landscape->RequestLayersContentUpdate(GetTickToolContentUpdateFlag());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual void EndTool(FEditorViewportClient* ViewportClient) override
|
|
{
|
|
if (IsToolActive() && InteractorPositions.Num())
|
|
{
|
|
ToolStroke->Apply(ViewportClient, EdMode->CurrentBrush, EdMode->UISettings, InteractorPositions);
|
|
InteractorPositions.Empty(1);
|
|
}
|
|
|
|
ToolStroke.Reset();
|
|
EdMode->CurrentBrush->EndStroke();
|
|
EdMode->UpdateLayerUsageInformation(&EdMode->CurrentToolTarget.LayerInfo);
|
|
|
|
if (ShouldUpdateEditingLayer())
|
|
{
|
|
ALandscape* Landscape = this->EdMode->GetLandscape();
|
|
if (Landscape)
|
|
{
|
|
Landscape->RequestLayersContentUpdate(GetEndToolContentUpdateFlag());
|
|
Landscape->SetEditingLayer();
|
|
Landscape->SetGrassUpdateEnabled(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y) override
|
|
{
|
|
if (ViewportClient != nullptr && Viewport != nullptr)
|
|
{
|
|
FVector HitLocation;
|
|
if (EdMode->LandscapeMouseTrace(ViewportClient, x, y, HitLocation))
|
|
{
|
|
// If we are moving the mouse to adjust the brush size, don't move the brush
|
|
if (EdMode->CurrentBrush && !EdMode->IsAdjustingBrush(ViewportClient))
|
|
{
|
|
// Inform the brush of the current location, to update the cursor
|
|
EdMode->CurrentBrush->MouseMove(HitLocation.X, HitLocation.Y);
|
|
}
|
|
|
|
if (IsToolActive())
|
|
{
|
|
// Save the interactor position
|
|
if (InteractorPositions.Num() == 0 || LastInteractorPosition != FVector2D(HitLocation))
|
|
{
|
|
LastInteractorPosition = FVector2D(HitLocation);
|
|
InteractorPositions.Emplace(LastInteractorPosition, IsModifierPressed(ViewportClient));
|
|
}
|
|
TimeSinceLastInteractorMove = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FVector2D NewPosition(x, y);
|
|
if (InteractorPositions.Num() == 0 || LastInteractorPosition != FVector2D(NewPosition))
|
|
{
|
|
LastInteractorPosition = FVector2D(NewPosition);
|
|
InteractorPositions.Emplace(LastInteractorPosition, IsModifierPressed());
|
|
}
|
|
TimeSinceLastInteractorMove = 0.0f;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual bool IsToolActive() const override { return ToolStroke.IsSet(); }
|
|
|
|
virtual void SetCanToolBeActivated(bool Value) { bCanToolBeActivated = Value; }
|
|
virtual bool CanToolBeActivated() const { return bCanToolBeActivated; }
|
|
|
|
protected:
|
|
TArray<FLandscapeToolInteractorPosition> InteractorPositions;
|
|
FVector2D LastInteractorPosition;
|
|
float TimeSinceLastInteractorMove;
|
|
FEdModeLandscape* EdMode;
|
|
bool bCanToolBeActivated;
|
|
TOptional<TStrokeClass> ToolStroke;
|
|
|
|
bool IsModifierPressed(const class FEditorViewportClient* ViewportClient = nullptr)
|
|
{
|
|
UE_LOG(LogLandscapeTools, VeryVerbose, TEXT("ViewportClient = %d, IsShiftDown = %d"), (ViewportClient != nullptr), (ViewportClient != nullptr && IsShiftDown(ViewportClient->Viewport)));
|
|
return ViewportClient != nullptr && IsShiftDown(ViewportClient->Viewport);
|
|
}
|
|
};
|
|
|
|
struct FToolFlattenCustomData
|
|
{
|
|
FToolFlattenCustomData()
|
|
: ActiveEyeDropperMode(false)
|
|
, EyeDropperModeHeight(0.0f)
|
|
{}
|
|
|
|
bool ActiveEyeDropperMode;
|
|
float EyeDropperModeHeight;
|
|
};
|