Add new plugins, refactor exp, introduce stat forge to replace GAS

This commit is contained in:
2026-07-15 22:48:04 +02:00
parent 3c084d9669
commit da0a0b643f
267 changed files with 33330 additions and 48 deletions

View File

@@ -0,0 +1,27 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "Noise Map Generator",
"Description": "Bake tiling noise textures with live preview.",
"Category": "Editor",
"CreatedBy": "UNmisterIZE(Sebastien Durocher)",
"CreatedByURL": "",
"DocsURL": "https://drive.google.com/file/d/134xAHp1LABTME2vNDhH0gcCkiqj1sL-o/view?usp=sharing",
"MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/2fd402da-2ee0-4532-b219-a0703790151d",
"SupportURL": "suroduro7@gmail.com",
"CanContainContent": true,
"Installed": true,
"Modules": [
{
"Name": "NoiseMapGenerator",
"Type": "Editor",
"LoadingPhase": "Default",
"PlatformAllowList": [
"Win64",
"Mac",
"Linux"
]
}
]
}

Binary file not shown.

View File

@@ -0,0 +1,24 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin */
using UnrealBuildTool;
public class NoiseMapGenerator : ModuleRules
{
public NoiseMapGenerator(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Slate", "SlateCore", "EditorStyle", "UnrealEd",
"Projects", "InputCore", "ToolMenus", "AssetTools",
"AssetRegistry", "RenderCore", "RHI", "DeveloperSettings"
});
}
}

View File

@@ -0,0 +1,508 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin
File: NoiseMapGenerator.cpp
*/
#include "NoiseMapGenerator.h"
#include "Engine/Texture2D.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Misc/PackageName.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
////// ---------------------------------------------------------
namespace TileableNoise
{
static FORCEINLINE float Fade(float t){ return t*t*t*(t*(t*6 - 15) + 10); }
static FORCEINLINE float Lerp(float a,float b,float t){ return a + t*(b - a); }
// Permutation table (512 entries = 256 duplicated)
struct Perm
{
TArray<int32> P;
explicit Perm(int32 Seed)
{
P.SetNumUninitialized(512);
TArray<int32> base; base.SetNumUninitialized(256);
for (int32 i=0;i<256;++i) base[i]=i;
FRandomStream RNG(Seed);
for (int32 i=255;i>0;--i){ const int32 j=RNG.RandRange(0,i); Swap(base[i],base[j]); }
for (int32 i=0;i<512;++i) P[i]=base[i & 255];
}
FORCEINLINE int32 H4(int32 x,int32 y,int32 z,int32 w) const
{ return P[P[P[P[x & 255] + (y & 255)] + (z & 255)] + (w & 255)]; }
};
// 4D gradient (32 dirs)
static FORCEINLINE float Grad4(int32 h, float x,float y,float z,float w)
{
const int32 b = h & 31;
switch (b)
{
case 0: return x + y + z + w; case 1: return x + y + z - w;
case 2: return x + y - z + w; case 3: return x + y - z - w;
case 4: return x - y + z + w; case 5: return x - y + z - w;
case 6: return x - y - z + w; case 7: return x - y - z - w;
case 8: return -x + y + z + w; case 9: return -x + y + z - w;
case 10: return -x + y - z + w; case 11: return -x + y - z - w;
case 12: return -x - y + z + w; case 13: return -x - y + z - w;
case 14: return -x - y - z + w; case 15: return -x - y - z - w;
case 16: return x + y + z; case 17: return x + y - z;
case 18: return x - y + z; case 19: return x - y - z;
case 20: return -x + y + z; case 21: return -x + y - z;
case 22: return -x - y + z; case 23: return -x - y - z;
case 24: return x + z + w; case 25: return x + z - w;
case 26: return x - z + w; case 27: return x - z - w;
case 28: return -x + z + w; case 29: return -x + z - w;
case 30: return -x - z + w; default: return -x - z - w;
}
}
static float Perlin4D(float x,float y,float z,float w,const Perm& perm)
{
const int32 X0 = FMath::FloorToInt(x), Y0 = FMath::FloorToInt(y), Z0 = FMath::FloorToInt(z), W0 = FMath::FloorToInt(w);
const float xf = x - X0, yf = y - Y0, zf = z - Z0, wf = w - W0;
const float u = Fade(xf), v = Fade(yf), s = Fade(zf), t = Fade(wf);
const int32 X1=X0+1,Y1=Y0+1,Z1=Z0+1,W1=W0+1;
const float n0000 = Grad4(perm.H4(X0,Y0,Z0,W0), xf, yf, zf, wf);
const float n1000 = Grad4(perm.H4(X1,Y0,Z0,W0), xf-1.f, yf, zf, wf);
const float n0100 = Grad4(perm.H4(X0,Y1,Z0,W0), xf, yf-1.f, zf, wf);
const float n1100 = Grad4(perm.H4(X1,Y1,Z0,W0), xf-1.f, yf-1.f, zf, wf);
const float n0010 = Grad4(perm.H4(X0,Y0,Z1,W0), xf, yf, zf-1.f, wf);
const float n1010 = Grad4(perm.H4(X1,Y0,Z1,W0), xf-1.f, yf, zf-1.f, wf);
const float n0110 = Grad4(perm.H4(X0,Y1,Z1,W0), xf, yf-1.f, zf-1.f, wf);
const float n1110 = Grad4(perm.H4(X1,Y1,Z1,W0), xf-1.f, yf-1.f, zf-1.f, wf);
const float n0001 = Grad4(perm.H4(X0,Y0,Z0,W1), xf, yf, zf, wf-1.f);
const float n1001 = Grad4(perm.H4(X1,Y0,Z0,W1), xf-1.f, yf, zf, wf-1.f);
const float n0101 = Grad4(perm.H4(X0,Y1,Z0,W1), xf, yf-1.f, zf, wf-1.f);
const float n1101 = Grad4(perm.H4(X1,Y1,Z0,W1), xf-1.f, yf-1.f, zf, wf-1.f);
const float n0011 = Grad4(perm.H4(X0,Y0,Z1,W1), xf, yf, zf-1.f, wf-1.f);
const float n1011 = Grad4(perm.H4(X1,Y0,Z1,W1), xf-1.f, yf, zf-1.f, wf-1.f);
const float n0111 = Grad4(perm.H4(X0,Y1,Z1,W1), xf, yf-1.f, zf-1.f, wf-1.f);
const float n1111 = Grad4(perm.H4(X1,Y1,Z1,W1), xf-1.f, yf-1.f, zf-1.f, wf-1.f);
const float nx00 = Lerp(n0000, n1000, u);
const float nx10 = Lerp(n0100, n1100, u);
const float nx01 = Lerp(n0010, n1010, u);
const float nx11 = Lerp(n0110, n1110, u);
const float nxy0 = Lerp(nx00, nx10, v);
const float nxy1 = Lerp(nx01, nx11, v);
const float nxyz0 = Lerp(nxy0, nxy1, s);
const float nx00b = Lerp(n0001, n1001, u);
const float nx10b = Lerp(n0101, n1101, u);
const float nx01b = Lerp(n0011, n1011, u);
const float nx11b = Lerp(n0111, n1111, u);
const float nxy0b = Lerp(nx00b, nx10b, v);
const float nxy1b = Lerp(nx01b, nx11b, v);
const float nxyz1 = Lerp(nxy0b, nxy1b, s);
return Lerp(nxyz0, nxyz1, t); // [-1,1]
}
static float FBM4D(float x,float y,float z,float w,const Perm& perm,int32 Octaves,float Lac,float Gain)
{
float amp=1.f,sum=0.f,range=0.f;
float X=x,Y=y,Z=z,W=w;
for (int32 o=0;o<Octaves;++o)
{
sum += amp * Perlin4D(X,Y,Z,W,perm);
range += amp;
X*=Lac; Y*=Lac; Z*=Lac; W*=Lac;
amp*=Gain;
}
return (range>0.f)? sum/range : 0.f;
}
// Hash utilities for Worley
static FORCEINLINE uint32 Hash2D(int32 x,int32 y,uint32 seed)
{
uint32 h = (uint32)x * 0x27d4eb2d ^ (uint32)y * 0x165667b1u ^ seed*0x9e3779b9u;
h ^= (h >> 16); h *= 0x7feb352d; h ^= (h >> 15); h *= 0x846ca68b; h ^= (h >> 16);
return h;
}
static FORCEINLINE float Frand01(uint32& state)
{
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
return (state & 0x00FFFFFF) / 16777216.0f;
}
// Torus-safe distance between (u,v) and feature in neighbor cell (iu,iv) with wrap
static float WorleyF1_Torus(float u,float v,int32 CellsU,int32 CellsV,uint32 seed)
{
// Base integer cell
const float fu = u * CellsU;
const float fv = v * CellsV;
const int32 iu0 = FMath::FloorToInt(fu);
const int32 iv0 = FMath::FloorToInt(fv);
float best = 1e9f;
for (int du=-1; du<=1; ++du)
for (int dv=-1; dv<=1; ++dv)
{
const int32 iu = (iu0 + du + CellsU) % CellsU;
const int32 iv = (iv0 + dv + CellsV) % CellsV;
uint32 st = Hash2D(iu, iv, seed);
const float jitterX = Frand01(st);
const float jitterY = Frand01(st);
const float fx = (float(iu) + jitterX) / float(CellsU);
const float fy = (float(iv) + jitterY) / float(CellsV);
// torus wrap distance in [0,1]
float dx = FMath::Abs(u - fx); dx = FMath::Min(dx, 1.0f - dx);
float dy = FMath::Abs(v - fy); dy = FMath::Min(dy, 1.0f - dy);
best = FMath::Min(best, FMath::Sqrt(dx*dx + dy*dy));
}
return best; // [0..~0.5]
}
}
/////// ---------------------------------------------------------
static void MakeUniqueAssetPath(const FString& Folder, const FString& BaseName, bool bOverwrite, FString& OutPkg, FString& OutAsset)
{
FString CleanFolder = Folder;
if (!CleanFolder.StartsWith(TEXT("/"))) CleanFolder = TEXT("/") + CleanFolder;
if (!CleanFolder.StartsWith(TEXT("/Game"))) CleanFolder = TEXT("/Game") + CleanFolder;
FString CandidateName = BaseName;
FString CandidatePkg = CleanFolder / CandidateName;
if (bOverwrite)
{
OutPkg = CandidatePkg;
OutAsset = CandidateName;
return;
}
int32 Suffix = 1;
while (FPackageName::DoesPackageExist(CandidatePkg))
{
CandidateName = FString::Printf(TEXT("%s_%03d"), *BaseName, Suffix++);
CandidatePkg = CleanFolder / CandidateName;
}
OutPkg = CandidatePkg;
OutAsset = CandidateName;
}
bool FNoiseMapGenerator::GeneratePixels(const FNoiseBakeSettings& S, int32 RepeatsUV, TArray<FColor>& OutPixels, int32& OutW, int32& OutH)
{
// --- size ---
const int32 W = S.Width;
const int32 H = S.Height;
if (W <= 0 || H <= 0)
{
OutW = OutH = 0;
OutPixels.Reset();
return false;
}
OutW = W;
OutH = H;
OutPixels.SetNumUninitialized(W * H);
// --- common setup ---
const float invW = 1.f / float(W);
const float invH = 1.f / float(H);
const float TwoPi = 6.28318530717958647692f;
const float Scale = 1.0f / FMath::Max(S.Frequency, 1e-6f);
TileableNoise::Perm perm(S.Seed);
// 4D torus mapping (period-1 in u,v)
auto AnglesFromUV = [&](float u, float v)
{
const float th = TwoPi * u;
const float ph = TwoPi * v;
struct { float X, Y, Z, W; } R
{
FMath::Cos(th) * Scale,
FMath::Sin(th) * Scale,
FMath::Cos(ph) * Scale,
FMath::Sin(ph) * Scale
};
return R;
};
// Base sampler per algorithm, returns [-1,1]
auto SampleBase = [&](float u, float v) -> float
{
switch (S.Algorithm)
{
case ENoiseAlgorithm::Perlin:
{
const auto C = AnglesFromUV(u, v);
return (S.Mode == ENoiseMode::FBM)
? TileableNoise::FBM4D(C.X, C.Y, C.Z, C.W, perm, S.Octaves, S.Lacunarity, S.Gain)
: TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm);
}
case ENoiseAlgorithm::RidgedFBM:
{
float amp = 0.5f;
float sum = 0.f;
float range = 0.f;
float freqMul = 1.f;
for (int32 o = 0; o < S.Octaves; ++o)
{
const float ou = FMath::Frac(u * freqMul);
const float ov = FMath::Frac(v * freqMul);
const auto C = AnglesFromUV(ou, ov);
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
const float r = 1.f - FMath::Abs(p); // [0,1]
sum += amp * r;
range += amp;
freqMul *= S.Lacunarity;
amp *= S.Gain;
}
return (range > 0.f) ? (sum / range) * 2.f - 1.f : 0.f;
}
case ENoiseAlgorithm::Worley:
{
const float cellsF = FMath::Clamp(
(1.0f / FMath::Max(S.Frequency, 1e-6f)) * 0.75f,
1.f, 512.f
);
const int32 CellsU = FMath::Max(1, int32(FMath::RoundToInt(cellsF)) * RepeatsUV);
const int32 CellsV = FMath::Max(1, int32(FMath::RoundToInt(cellsF)) * RepeatsUV);
const float d = TileableNoise::WorleyF1_Torus(u, v, CellsU, CellsV, (uint32)S.Seed); // [0..~0.5]
const float v01 = 1.0f - FMath::Clamp(d * 2.0f, 0.f, 1.f);
return v01 * 2.f - 1.f;
}
default:
return 0.f;
}
};
auto SampleNthOctave01 = [&](float u, float v, int32 OctIdx) -> float
{
switch (S.Algorithm)
{
case ENoiseAlgorithm::Perlin:
{
// Higher octave = higher frequency
const float freqMul = FMath::Pow(S.Lacunarity, OctIdx);
const auto C = AnglesFromUV(u * freqMul, v * freqMul);
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
return 0.5f * (p + 1.f); // → [0,1]
}
case ENoiseAlgorithm::RidgedFBM:
{
const float freqMul = FMath::Pow(S.Lacunarity, OctIdx);
const float ou = FMath::Frac(u * freqMul);
const float ov = FMath::Frac(v * freqMul);
const auto C = AnglesFromUV(ou, ov);
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
const float r = 1.f - FMath::Abs(p); // [0,1] "ridge" shape
return r;
}
case ENoiseAlgorithm::Worley:
{
// More cells per octave for finer detail
const float cellsFBase = FMath::Clamp(
(1.0f / FMath::Max(S.Frequency, 1e-6f)) * 0.75f,
1.f, 512.f
);
const float octaveMul = FMath::Pow(S.Lacunarity, OctIdx);
const int32 CellsU = FMath::Max(1, int32(FMath::RoundToInt(cellsFBase * octaveMul)) * RepeatsUV);
const int32 CellsV = FMath::Max(1, int32(FMath::RoundToInt(cellsFBase * octaveMul)) * RepeatsUV);
const float d = TileableNoise::WorleyF1_Torus(u, v, CellsU, CellsV, (uint32)S.Seed);
const float v01 = 1.0f - FMath::Clamp(d * 2.0f, 0.f, 1.f); // [0,1], "cells" as bright
return v01;
}
default:
// Fallback mid-gray
return 0.5f;
}
};
// Write pixels
if (S.OutputMode == ENoiseOutputMode::Grayscale)
{
float minV = 1e9f;
float maxV = -1e9f;
TArray<float> tmp;
tmp.SetNumUninitialized(W * H);
int32 I = 0;
for (int32 y = 0; y < H; ++y)
{
for (int32 x = 0; x < W; ++x)
{
const float u = float(RepeatsUV) * float(x) * invW;
const float v = float(RepeatsUV) * float(y) * invH;
const float vraw = SampleBase(u, v); // [-1,1]
tmp[I++] = vraw;
minV = FMath::Min(minV, vraw);
maxV = FMath::Max(maxV, vraw);
}
}
const float invRange =
(S.bNormalize && maxV > minV) ? 1.f / (maxV - minV) : 0.5f;
I = 0;
for (int32 y = 0; y < H; ++y)
{
for (int32 x = 0; x < W; ++x)
{
float v = tmp[I++];
v = S.bNormalize
? (v - minV) * invRange // [0,1]
: 0.5f * (v + 1.f); // [-1,1] → [0,1]
const uint8 g = (uint8)FMath::Clamp(
FMath::RoundToInt(v * 255.f), 0, 255);
OutPixels[y * W + x] = FColor(g, g, g, 255);
}
}
}
else // Split First 3 Octaves to RGB channles
{
for (int32 y = 0; y < H; ++y)
{
for (int32 x = 0; x < W; ++x)
{
const float u = float(RepeatsUV) * float(x) * invW;
const float v = float(RepeatsUV) * float(y) * invH;
const float r = SampleNthOctave01(u, v, 0);
const float g = SampleNthOctave01(u, v, 1);
const float b = SampleNthOctave01(u, v, 2);
OutPixels[y * W + x] = FColor(
(uint8)FMath::Clamp(FMath::RoundToInt(r * 255.f), 0, 255),
(uint8)FMath::Clamp(FMath::RoundToInt(g * 255.f), 0, 255),
(uint8)FMath::Clamp(FMath::RoundToInt(b * 255.f), 0, 255),
255);
}
}
}
return true;
}
void FNoiseMapGenerator::BakeFromPixels(
const FNoiseBakeSettings& S,
const TArray<FColor>& Pixels,
int32 W,
int32 H)
{
if (W <= 0 || H <= 0 || Pixels.Num() != W * H)
{
return;
}
// --- Resolve package path/name ---
FString OutPath = S.OutputPath.IsEmpty()
? TEXT("/Game/Noise")
: S.OutputPath;
if (!OutPath.StartsWith(TEXT("/Game")))
{
OutPath = TEXT("/Game/Noise");
}
FString PackageName, AssetName;
MakeUniqueAssetPath(OutPath, S.BaseName, S.bOverwrite, PackageName, AssetName);
UPackage* Package = CreatePackage(*PackageName);
if (!Package)
{
return;
}
UTexture2D* Tex = NewObject<UTexture2D>(
Package,
*AssetName,
RF_Public | RF_Standalone
);
if (!Tex)
{
return;
}
Tex->Modify();
// Match preview flags
Tex->SRGB = false;
if (S.OutputMode == ENoiseOutputMode::Grayscale)
{
Tex->CompressionSettings = TC_Grayscale;
}
else
{
Tex->CompressionSettings = TC_VectorDisplacementmap; // linear RGB
}
Tex->CompressionNone = true;
Tex->DeferCompression = false;
Tex->MipGenSettings = TMGS_NoMipmaps;
Tex->AddressX = TA_Wrap;
Tex->AddressY = TA_Wrap;
Tex->LODGroup = TEXTUREGROUP_World;
// Copy pixels into source mip
Tex->Source.Init(W, H, 1, 1, TSF_BGRA8);
{
uint8* Src = Tex->Source.LockMip(0);
const int64 Bytes = int64(W) * H * sizeof(FColor);
FMemory::Memcpy(Src, Pixels.GetData(), Bytes);
Tex->Source.UnlockMip(0);
}
Tex->PostEditChange();
Tex->UpdateResource();
FAssetRegistryModule::AssetCreated(Tex);
Package->MarkPackageDirty();
const FString PkgFile =
FPackageName::LongPackageNameToFilename(
PackageName,
FPackageName::GetAssetPackageExtension()
);
FSavePackageArgs Args;
Args.TopLevelFlags = RF_Public | RF_Standalone;
Args.SaveFlags = SAVE_None;
Args.Error = GError;
Args.bWarnOfLongFilename = true;
UPackage::SavePackage(Package, Tex, *PkgFile, Args);
}
void FNoiseMapGenerator::BakeNoiseTexture(const FNoiseBakeSettings& S)
{
TArray<FColor> Pixels;
int32 W = 0, H = 0;
// IMPORTANT: RepeatsUV = 1 for a single seamless period
if (!GeneratePixels(S, /*RepeatsUV=*/1, Pixels, W, H))
{
return;
}
BakeFromPixels(S, Pixels, W, H);
}

View File

@@ -0,0 +1,416 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin
File: NoiseMapGeneratorModule.cpp
*/
#include "NoiseMapGeneratorModule.h"
#include "NoiseMapGenerator.h"
#include "NoiseMapGeneratorSettings.h"
#include "Widgets/Docking/SDockTab.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Input/SSpinBox.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "ToolMenus.h"
#include "LevelEditor.h"
#include "Engine/Texture2D.h"
#include "ContentBrowserModule.h"
#include "IContentBrowserSingleton.h"
#include "Modules/ModuleManager.h"
static const FName NoiseMapGenTabName("NoiseMapGeneratorTab");
// Preview helper
static UTexture2D* CreateTransientTexture(int32 W, int32 H, const TArray<FColor>& Pixels)
{
if (Pixels.Num() != W * H) return nullptr;
UTexture2D* T = UTexture2D::CreateTransient(W, H, PF_B8G8R8A8);
if (!T) return nullptr;
T->SRGB = false;
T->CompressionSettings = TC_Default;
T->MipGenSettings = TMGS_NoMipmaps;
FTexture2DMipMap& Mip = T->GetPlatformData()->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
const int64 Bytes = (int64)W * H * sizeof(FColor);
FMemory::Memcpy(Data, Pixels.GetData(), Bytes);
Mip.BulkData.Unlock();
T->UpdateResource();
return T;
}
// Plugin UI setup
class SNoiseMapGeneratorWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SNoiseMapGeneratorWidget) {}
SLATE_END_ARGS()
void Construct(const FArguments&)
{
Settings = GetMutableDefault<UNoiseMapGeneratorSettings>();
PreviewBrush = MakeShared<FSlateBrush>();
ChildSlot
[
SNew(SBorder)
[
SNew(SSplitter)
+ SSplitter::Slot().Value(0.33f)
[
SNew(SBorder)
[
SNew(SBox)
.MinDesiredWidth(580.f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ SNew(STextBlock).Text(FText::FromString("Noise Settings")) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Width", Settings->Width, 8, 8192, 64) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Height", Settings->Height, 8, 8192, 64) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Preview Tiling", Settings->TileRepeats, 1, 64, 1) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Seed", Settings->Seed, 0, INT_MAX, 1) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Octaves", Settings->Octaves, 1, 12, 1) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Frequency", Settings->Frequency, 0.001f, 2.0f, 0.005f) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Lacunarity", Settings->Lacunarity, 1.f, 8.f, 0.1f) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Gain", Settings->Gain, 0.f, 1.f, 0.05f) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Normalize 0..1", Settings->bNormalize) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowMode() ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowOutputMode() ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowAlgorithm() ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowText("Save Path (/Game/...)", Settings->OutputPath) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowText("Base Name", Settings->BaseName) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Overwrite Existing", Settings->bOverwrite) ]
// + SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Generate Mips", Settings->bGenerateMips) ]
+ SVerticalBox::Slot().AutoHeight().Padding(6)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().Padding(0,0,6,0)
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnGenerate)
[ SNew(STextBlock).Text(FText::FromString("Generate Preview")) ] ]
+ SHorizontalBox::Slot().AutoWidth().Padding(0,0,6,0)
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnBake)
[ SNew(STextBlock).Text(FText::FromString("Bake & Save")) ] ]
+ SHorizontalBox::Slot().AutoWidth()
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnQuit)
[ SNew(STextBlock).Text(FText::FromString("Quit")) ] ]
]
]
]
]
+ SSplitter::Slot().Value(0.67f)
[
SNew(SBorder)
[ SAssignNew(PreviewImage, SImage).Image(nullptr) ]
]
]
];
Regenerate();
}
private:
// UI helpers
TSharedRef<SWidget> MakeRowInt(const char* Label, int32& V, int32 Min, int32 Max, int32 Step)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
+ SHorizontalBox::Slot().FillWidth(1.f)
[ SNew(SSpinBox<int32>).MinValue(Min).MaxValue(Max).Delta(Step)
.Value_Lambda([&V]{ return V; })
.OnValueChanged_Lambda([&V](int32 NV){ V = NV; }) ];
}
TSharedRef<SWidget> MakeRowFloat(const char* Label, float& V, float Min, float Max, float Step)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
+ SHorizontalBox::Slot().FillWidth(1.f)
[ SNew(SSpinBox<float>).MinValue(Min).MaxValue(Max).Delta(Step)
.Value_Lambda([&V]{ return V; })
.OnValueChanged_Lambda([&V](float NV){ V = NV; }) ];
}
TSharedRef<SWidget> MakeRowBool(const char* Label, bool& B)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
[ SNew(SCheckBox)
.IsChecked_Lambda([&B]{ return B ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
.OnCheckStateChanged_Lambda([&B](ECheckBoxState S){ B = (S == ECheckBoxState::Checked); }) ];
}
TSharedRef<SWidget> MakeRowText(const char* Label, FString& T)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
+ SHorizontalBox::Slot().FillWidth(1.f)
[ SNew(SEditableTextBox)
.Text_Lambda([&T]{ return FText::FromString(T); })
.OnTextCommitted_Lambda([&T](const FText& New, ETextCommit::Type){ T = New.ToString(); }) ];
}
TSharedRef<SWidget> MakeRowMode()
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString("Mode")) ]
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
[ SNew(SCheckBox)
.OnCheckStateChanged_Lambda([this](ECheckBoxState S){ Settings->Mode = (S == ECheckBoxState::Checked) ? ENoiseMode::FBM : ENoiseMode::Perlin; Regenerate(); })
.IsChecked_Lambda([this]{ return Settings->Mode == ENoiseMode::FBM ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
.Content()[ SNew(STextBlock).Text(FText::FromString("FBM (unchecked = Perlin)")) ] ];
}
TSharedRef<SWidget> MakeRowOutputMode()
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString("Output")) ]
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
[ SNew(SCheckBox)
.OnCheckStateChanged_Lambda([this](ECheckBoxState S){ Settings->OutputMode = (S == ECheckBoxState::Checked) ? ENoiseOutputMode::SplitFirst3Octaves : ENoiseOutputMode::Grayscale; Regenerate(); })
.IsChecked_Lambda([this]{ return Settings->OutputMode == ENoiseOutputMode::SplitFirst3Octaves ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
.Content()[ SNew(STextBlock).Text(FText::FromString("Split 3 Octaves to RGB")) ] ];
}
// ---------------------------
static FString AlgoToString(ENoiseAlgorithm A)
{
switch (A)
{
case ENoiseAlgorithm::Perlin: return TEXT("Perlin");
case ENoiseAlgorithm::Worley: return TEXT("Worley (Cellular)");
case ENoiseAlgorithm::RidgedFBM: return TEXT("Ridged FBM");
default: return TEXT("Perlin");
}
}
TSharedRef<SWidget> OnGenerateAlgoWidget(TSharedPtr<ENoiseAlgorithm> InItem)
{
return SNew(STextBlock).Text(FText::FromString(AlgoToString(*InItem)));
}
void OnAlgoChanged(TSharedPtr<ENoiseAlgorithm> NewSel, ESelectInfo::Type)
{
if (!NewSel.IsValid()) return;
Settings->Algorithm = *NewSel;
SelectedAlgo = NewSel;
Regenerate();
}
FText GetAlgoCurrentText() const
{
return FText::FromString(AlgoToString(Settings->Algorithm));
}
TSharedRef<SWidget> MakeRowAlgorithm()
{
// Populate items once if empty
if (AlgoItems.Num() == 0)
{
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::Perlin));
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::Worley));
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::RidgedFBM));
// pick current
for (const auto& It : AlgoItems)
{
if (*It == Settings->Algorithm)
{
SelectedAlgo = It;
break;
}
}
if (!SelectedAlgo.IsValid())
{
SelectedAlgo = AlgoItems[0];
}
}
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)
[ SNew(STextBlock).Text(FText::FromString("Algorithm")) ]
+ SHorizontalBox::Slot().FillWidth(1.f)
[
SNew(SComboBox<TSharedPtr<ENoiseAlgorithm>>)
.OptionsSource(&AlgoItems)
.InitiallySelectedItem(SelectedAlgo)
.OnGenerateWidget(this, &SNoiseMapGeneratorWidget::OnGenerateAlgoWidget)
.OnSelectionChanged(this, &SNoiseMapGeneratorWidget::OnAlgoChanged)
[
SNew(STextBlock).Text_Lambda([this]{ return GetAlgoCurrentText(); })
]
];
}
TArray<TSharedPtr<ENoiseAlgorithm>> AlgoItems;
TSharedPtr<ENoiseAlgorithm> SelectedAlgo;
FReply OnGenerate()
{
Regenerate();
return FReply::Handled();
}
FReply OnBake()
{
FNoiseBakeSettings B;
B.Width = Settings->Width;
B.Height = Settings->Height;
B.TileRepeats = Settings->TileRepeats;
B.Seed = Settings->Seed;
B.Octaves = Settings->Octaves;
B.Frequency = Settings->Frequency;
B.Lacunarity = Settings->Lacunarity;
B.Gain = Settings->Gain;
B.bNormalize = Settings->bNormalize;
B.Mode = Settings->Mode;
B.OutputMode = Settings->OutputMode;
B.OutputPath = Settings->OutputPath;
B.BaseName = Settings->BaseName;
B.bOverwrite = Settings->bOverwrite;
//B.bGenerateMips = Settings->bGenerateMips;
B.Algorithm = Settings->Algorithm;
// Calls GeneratePixels then bakes with the same flags as preview helper.
FNoiseMapGenerator::BakeNoiseTexture(B);
// Open content browser at location
{
FString FolderPathStr = Settings->OutputPath;
if (!FolderPathStr.StartsWith(TEXT("/Game")))
{
FolderPathStr = TEXT("/Game");
}
TArray<FString> Folders;
Folders.Add(FolderPathStr);
IContentBrowserSingleton& CB =
FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser").Get();
CB.SyncBrowserToFolders(Folders);
}
return FReply::Handled();
}
FReply OnQuit()
{
TSharedPtr<SDockTab> Tab = FGlobalTabmanager::Get()->FindExistingLiveTab(NoiseMapGenTabName);
if (Tab.IsValid())
{
Tab->RequestCloseTab();
}
return FReply::Handled();
}
void Regenerate()
{
FNoiseBakeSettings B;
B.Width=Settings->Width; B.Height=Settings->Height; B.TileRepeats=Settings->TileRepeats; B.Seed=Settings->Seed;
B.Octaves=Settings->Octaves; B.Frequency=Settings->Frequency; B.Lacunarity=Settings->Lacunarity; B.Gain=Settings->Gain;
B.bNormalize=Settings->bNormalize; B.Mode=Settings->Mode; B.OutputMode=Settings->OutputMode;
B.OutputPath=Settings->OutputPath; B.BaseName=Settings->BaseName; B.bOverwrite=Settings->bOverwrite; //B.bGenerateMips=Settings->bGenerateMips;
B.Algorithm = Settings->Algorithm;
TArray<FColor> Pixels; int32 W=0, H=0;
if (FNoiseMapGenerator::GeneratePixels(B,Settings->TileRepeats, Pixels, W, H))
{
if (UTexture2D* T = CreateTransientTexture(W, H, Pixels))
{
if (!PreviewBrush.IsValid()) PreviewBrush = MakeShareable(new FSlateBrush());
PreviewBrush->ImageSize = FVector2D(W, H);
PreviewBrush->SetResourceObject(T);
PreviewImage->SetImage(PreviewBrush.Get());
}
}
};
private:
UNoiseMapGeneratorSettings* Settings = nullptr;
TSharedPtr<SImage> PreviewImage;
TSharedPtr<FSlateBrush> PreviewBrush;
};
void FNoiseMapGeneratorModule::StartupModule()
{
if (FGlobalTabmanager::Get()->HasTabSpawner(NoiseMapGenTabName))
{
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(NoiseMapGenTabName);
}
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
NoiseMapGenTabName,
FOnSpawnTab::CreateLambda([](const FSpawnTabArgs&)
{
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
SNew(SNoiseMapGeneratorWidget)
];
})
)
.SetDisplayName(FText::FromString("Noise Map Generator"))
.SetMenuType(ETabSpawnerMenuType::Hidden);
if (UToolMenus::IsToolMenuUIEnabled())
{
RegisterMenus();
}
else
{
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(
this, &FNoiseMapGeneratorModule::RegisterMenus));
}
}
void FNoiseMapGeneratorModule::ShutdownModule()
{
if (FGlobalTabmanager::Get()->HasTabSpawner(NoiseMapGenTabName))
{
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(NoiseMapGenTabName);
}
UToolMenus::UnregisterOwner(this);
}
void FNoiseMapGeneratorModule::OpenWindow()
{
FGlobalTabmanager::Get()->TryInvokeTab(NoiseMapGenTabName);
}
void FNoiseMapGeneratorModule::RegisterMenus()
{
FToolMenuOwnerScoped OwnerScoped(this);
if (UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window"))
{
// Insert a custom section at the top.
FToolMenuSection& TopSection = Menu->AddSection(
"NoiseMapGeneratorSection",
FText::GetEmpty(),
FToolMenuInsert(NAME_None, EToolMenuInsertType::First)
);
TopSection.AddMenuEntry(
"OpenNoiseMapGenerator",
FText::FromString("Noise Map Generator"),
FText::FromString("Open the Noise Map Generator window"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateRaw(this, &FNoiseMapGeneratorModule::OpenWindow))
);
}
}
IMPLEMENT_MODULE(FNoiseMapGeneratorModule, NoiseMapGenerator)

View File

@@ -0,0 +1,8 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin
File: NoiseMapGeneratorSettings.cpp
*/
#include "NoiseMapGeneratorSettings.h"

View File

@@ -0,0 +1,40 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin
File: NoiseMapGenerator.h
*/
#pragma once
#include "CoreMinimal.h"
#include "NoiseMapGeneratorSettings.h"
class UTexture2D;
struct FNoiseBakeSettings
{
int32 Width = 1024;
int32 Height = 1024;
int32 TileRepeats = 1;
int32 Seed = 1337;
int32 Octaves = 5;
float Frequency = 0.25f;
float Lacunarity = 2.0f;
float Gain = 0.5f;
bool bNormalize = true;
ENoiseMode Mode = ENoiseMode::FBM;
ENoiseAlgorithm Algorithm = ENoiseAlgorithm::Perlin;
ENoiseOutputMode OutputMode = ENoiseOutputMode::Grayscale;
FString OutputPath = TEXT("/Game/Noise");
FString BaseName = TEXT("T_Noise");
bool bOverwrite = false;
bool bGenerateMips = false;
};
class NOISEMAPGENERATOR_API FNoiseMapGenerator
{
public:
static bool GeneratePixels(const FNoiseBakeSettings& S, int32 RepeatsUV, TArray<FColor>& OutPixels, int32& OutW, int32& OutH);
static void BakeNoiseTexture(const FNoiseBakeSettings& Settings);
static void BakeFromPixels(const FNoiseBakeSettings& S, const TArray<FColor>& Pixels, int32 W, int32 H);
};

View File

@@ -0,0 +1,22 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin
File: NoiseMapGeneratorModule.h
*/
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "ToolMenus.h"
class FNoiseMapGeneratorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
void OpenWindow();
void RegisterMenus();
};

View File

@@ -0,0 +1,103 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin
File: NoiseMapGeneratorSettings.h
*/
#pragma once
#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "NoiseMapGeneratorSettings.generated.h"
UENUM()
enum class ENoiseMode : uint8
{
Perlin,
FBM
};
UENUM()
enum class ENoiseOutputMode : uint8
{
Grayscale,
SplitFirst3Octaves
};
UENUM()
enum class ENoiseAlgorithm : uint8
{
Perlin UMETA(DisplayName="Perlin"),
Worley UMETA(DisplayName="Worley (Cellular)"),
RidgedFBM UMETA(DisplayName="Ridged FBM")
};
UCLASS(config=EditorPerProjectUserSettings, defaultconfig)
class UNoiseMapGeneratorSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
virtual FText GetSectionText() const override { return FText::FromString(TEXT("Noise Map Generator")); }
// Resolution
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="8", ClampMax="16384"))
int32 Width = 1024;
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="8", ClampMax="16384"))
int32 Height = 1024;
// Tiling
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1", ClampMax="64"))
int32 TileRepeats = 1;
// Noise parameters
UPROPERTY(EditAnywhere, config, Category = "Noise")
int32 Seed = 1337;
// Used by FBM (ignored for Perlin-only grayscale)
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1", ClampMax="12"))
int32 Octaves = 5;
// features size
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="0.001", ClampMax="2.0"))
float Frequency = 0.5f;
// Frequency multiplier per octave
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1.0", ClampMax="8.0"))
float Lacunarity = 2.0f;
// Amplitude multiplier per octave
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="0.0", ClampMax="1.0"))
float Gain = 0.5f;
// Normalize result to [0..1] (if off, uses 0.5*(v+1))
UPROPERTY(EditAnywhere, config, Category = "Noise")
bool bNormalize = true;
// Perlin or FBM for grayscale output
UPROPERTY(EditAnywhere, config, Category = "Noise")
ENoiseMode Mode = ENoiseMode::FBM;
// Which base algorithm to use
UPROPERTY(EditAnywhere, config, Category = "Noise")
ENoiseAlgorithm Algorithm = ENoiseAlgorithm::Perlin;
// Grayscale or pack first 3 octaves into RGB
UPROPERTY(EditAnywhere, config, Category = "Noise")
ENoiseOutputMode OutputMode = ENoiseOutputMode::Grayscale;
// Save opt
UPROPERTY(EditAnywhere, config, Category = "Save")
FString OutputPath = TEXT("/Game/Noise");
UPROPERTY(EditAnywhere, config, Category = "Save")
FString BaseName = TEXT("T_Noise");
UPROPERTY(EditAnywhere, config, Category = "Save")
bool bOverwrite = false;
//UPROPERTY(EditAnywhere, config, Category = "Save")
// bool bGenerateMips = false;
};