Add new plugins, refactor exp, introduce stat forge to replace GAS
This commit is contained in:
27
Plugins/NoiseMapGenerator/NoiseMapGenerator.uplugin
Normal file
27
Plugins/NoiseMapGenerator/NoiseMapGenerator.uplugin
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/NoiseMapGenerator/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/NoiseMapGenerator/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
@@ -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"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorSettings.cpp
|
||||
*/
|
||||
|
||||
#include "NoiseMapGeneratorSettings.h"
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user