🔍 s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
🔗 Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=sturnus.terraingenerationtool&take=20
Showing code results for query:
*
(14 total matches found)
Editor
library
using System.Runtime.CompilerServices;
public static class OpenSimplex2S
{
private const long PRIME_X = 0x5205402B9270C86FL;
private const long PRIME_Y = 0x598CD327003817B5L;
private const long PRIME_Z = 0x5BCC226E9FA0BACBL;
private const long PRIME_W = 0x56CC5227E58F554BL;
private const long HASH_MULTIPLIER = 0x53A3F72DEEC546F5L;
private const long SEED_FLIP_3D = -0x52D547B2E96ED629L;
private const double ROOT2OVER2 = 0.7071067811865476;
private const double SKEW_2D = 0.366025403784439;
private const double UNSKEW_2D = -0.21132486540518713;
private const double ROOT3OVER3 = 0.577350269189626;
private const double FALLBACK_ROTATE3 = 2.0 / 3.0;
private const double ROTATE3_ORTHOGONALIZER = UNSKEW_2D;
private const float SKEW_4D = 0.309016994374947f;
private const float UNSKEW_4D = -0.138196601125011f;
private const int N_GRADS_2D_EXPONENT = 7;
private const int N_GRADS_3D_EXPONENT = 8;
private const int N_GRADS_4D_EXPONENT = 9;
private const int N_GRADS_2D = 1 << N_GRADS_2D_EXPONENT;
private const int N_GRADS_3D = 1 << N_GRADS_3D_EXPONENT;
private const int N_GRADS_4D = 1 << N_GRADS_4D_EXPONENT;
private const double NORMALIZER_2D = 0.05481866495625118;
private const double NORMALIZER_3D = 0.2781926117527186;
private const double NORMALIZER_4D = 0.11127401889945551;
private const float RSQUARED_2D = 2.0f / 3.0f;
private const float RSQUARED_3D = 3.0f / 4.0f;
private const float RSQUARED_4D = 4.0f / 5.0f;
/*
* Noise Evaluators
*/
/**
* 2D OpenSimplex2S/SuperSimplex noise, standard lattice orientation.
*/
public static float Noise2( long seed, double x, double y )
{
// Get points for A2* lattice
double s = SKEW_2D * (x + y);
double xs = x + s, ys = y + s;
return Noise2_UnskewedBase( seed, xs, ys );
}
/**
* 2D OpenSimplex2S/SuperSimplex noise, with Y pointing down the main diagonal.
* Might be better for a 2D sandbox style game, where Y is vertical.
* Probably slightly less optimal for heightmaps or continent maps,
* unless your map is centered around an equator. It's a slight
* difference, but the option is here to make it easy.
*/
public static float Noise2_ImproveX( long seed, double x, double y )
{
// Skew transform and rotation baked into one.
double xx = x * ROOT2OVER2;
double yy = y * (ROOT2OVER2 * (1 + 2 * SKEW_2D));
return Noise2_UnskewedBase( seed, yy + xx, yy - xx );
}
/**
* 2D OpenSimplex2S/SuperSimplex noise base.
*/
private static float Noise2_UnskewedBase( long seed, double xs, double ys )
{
// Get base points and offsets.
int xsb = FastFloor( xs ), ysb = FastFloor( ys );
float xi = (float)(xs - xsb), yi = (float)(ys - ysb);
// Prime pre-multiplication for hash.
long xsbp = xsb * PRIME_X, ysbp = ysb * PRIME_Y;
// Unskew.
float t = (xi + yi) * (float)UNSKEW_2D;
float dx0 = xi + t, dy0 = yi + t;
// First vertex.
float a0 = RSQUARED_2D - dx0 * dx0 - dy0 * dy0;
float value = (a0 * a0) * (a0 * a0) * Grad( seed, xsbp, ysbp, dx0, dy0 );
// Second vertex.
float a1 = (float)(2 * (1 + 2 * UNSKEW_2D) * (1 / UNSKEW_2D + 2)) * t + ((float)(-2 * (1 + 2 * UNSKEW_2D) * (1 + 2 * UNSKEW_2D)) + a0);
float dx1 = dx0 - (float)(1 + 2 * UNSKEW_2D);
float dy1 = dy0 - (float)(1 + 2 * UNSKEW_2D);
value += (a1 * a1) * (a1 * a1) * Grad( seed, xsbp + PRIME_X, ysbp + PRIME_Y, dx1, dy1 );
// Third and fourth vertices.
// Nested conditionals were faster than compact bit logic/arithmetic.
float xmyi = xi - yi;
if ( t < UNSKEW_2D )
{
if ( xi + xmyi > 1 )
{
float dx2 = dx0 - (float)(3 * UNSKEW_2D + 2);
float dy2 = dy0 - (float)(3 * UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp + (PRIME_X << 1), ysbp + PRIME_Y, dx2, dy2 );
}
}
else
{
float dx2 = dx0 - (float)UNSKEW_2D;
float dy2 = dy0 - (float)(UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp + PRIME_Y, dx2, dy2 );
}
}
if ( yi - xmyi > 1 )
{
float dx3 = dx0 - (float)(3 * UNSKEW_2D + 1);
float dy3 = dy0 - (float)(3 * UNSKEW_2D + 2);
float a3 = RSQUARED_2D - dx3 * dx3 - dy3 * dy3;
if ( a3 > 0 )
{
value += (a3 * a3) * (a3 * a3) * Grad( seed, xsbp + PRIME_X, ysbp + (PRIME_Y << 1), dx3, dy3 );
}
}
else
{
float dx3 = dx0 - (float)(UNSKEW_2D + 1);
float dy3 = dy0 - (float)UNSKEW_2D;
float a3 = RSQUARED_2D - dx3 * dx3 - dy3 * dy3;
if ( a3 > 0 )
{
value += (a3 * a3) * (a3 * a3) * Grad( seed, xsbp + PRIME_X, ysbp, dx3, dy3 );
}
}
}
else
{
if ( xi + xmyi < 0 )
{
float dx2 = dx0 + (float)(1 + UNSKEW_2D);
float dy2 = dy0 + (float)UNSKEW_2D;
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp - PRIME_X, ysbp, dx2, dy2 );
}
}
else
{
float dx2 = dx0 - (float)(UNSKEW_2D + 1);
float dy2 = dy0 - (float)UNSKEW_2D;
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp + PRIME_X, ysbp, dx2, dy2 );
}
}
if ( yi < xmyi )
{
float dx2 = dx0 + (float)UNSKEW_2D;
float dy2 = dy0 + (float)(UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp - PRIME_Y, dx2, dy2 );
}
}
else
{
float dx2 = dx0 - (float)UNSKEW_2D;
float dy2 = dy0 - (float)(UNSKEW_2D + 1);
float a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;
if ( a2 > 0 )
{
value += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp + PRIME_Y, dx2, dy2 );
}
}
}
return value;
}
/**
* 3D OpenSimplex2S/SuperSimplex noise, with better visual isotropy in (X, Y).
* Recommended for 3D terrain and time-varied animations.
* The Z coordinate should always be the "different" coordinate in whatever your use case is.
* If Y is vertical in world coordinates, call Noise3_ImproveXZ(x, z, Y) or use Noise3_XZBeforeY.
* If Z is vertical in world coordinates, call Noise3_ImproveXZ(x, y, Z).
* For a time varied animation, call Noise3_ImproveXY(x, y, T).
*/
public static float Noise3_ImproveXY( long seed, double x, double y, double z )
{
// Re-orient the cubic lattices without skewing, so Z points up the main lattice diagonal,
// and the planes formed by XY are moved far out of alignment with the cube faces.
// Orthonormal rotation. Not a skew transform.
double xy = x + y;
double s2 = xy * ROTATE3_ORTHOGONALIZER;
double zz = z * ROOT3OVER3;
double xr = x + s2 + zz;
double yr = y + s2 + zz;
double zr = xy * -ROOT3OVER3 + zz;
// Evaluate both lattices to form a BCC lattice.
return Noise3_UnrotatedBase( seed, xr, yr, zr );
}
/**
* 3D OpenSimplex2S/SuperSimplex noise, with better visual isotropy in (X, Z).
* Recommended for 3D terrain and time-varied animations.
* The Y coordinate should always be the "different" coordinate in whatever your use case is.
* If Y is vertical in world coordinates, call Noise3_ImproveXZ(x, Y, z).
* If Z is vertical in world coordinates, call Noise3_ImproveXZ(x, Z, y) or use Noise3_ImproveXY.
* For a time varied animation, call Noise3_ImproveXZ(x, T, y) or use Noise3_ImproveXY.
*/
public static float Noise3_ImproveXZ( long seed, double x, double y, double z )
{
// Re-orient the cubic lattices without skewing, so Y points up the main lattice diagonal,
// and the planes formed by XZ are moved far out of alignment with the cube faces.
// Orthonormal rotation. Not a skew transform.
double xz = x + z;
double s2 = xz * -0.211324865405187;
double yy = y * ROOT3OVER3;
double xr = x + s2 + yy;
double zr = z + s2 + yy;
double yr = xz * -ROOT3OVER3 + yy;
// Evaluate both lattices to form a BCC lattice.
return Noise3_UnrotatedBase( seed, xr, yr, zr );
}
/**
* 3D OpenSimplex2S/SuperSimplex noise, fallback rotation option
* Use Noise3_ImproveXY or Noise3_ImproveXZ instead, wherever appropriate.
* They have less diagonal bias. This function's best use is as a fallback.
*/
public static float Noise3_Fallback( long seed, double x, double y, double z )
{
// Re-orient the cubic lattices via rotation, to produce a familiar look.
// Orthonormal rotation. Not a skew transform.
double r = FALLBACK_ROTATE3 * (x + y + z);
double xr = r - x, yr = r - y, zr = r - z;
// Evaluate both lattices to form a BCC lattice.
return Noise3_UnrotatedBase( seed, xr, yr, zr );
}
/**
* Generate overlapping cubic lattices for 3D Re-oriented BCC noise.
* Lookup table implementation inspired by DigitalShadow.
* It was actually faster to narrow down the points in the loop itself,
* than to build up the index with enough info to isolate 8 points.
*/
private static float Noise3_UnrotatedBase( long seed, double xr, double yr, double zr )
{
// Get base points and offsets.
int xrb = FastFloor( xr ), yrb = FastFloor( yr ), zrb = FastFloor( zr );
float xi = (float)(xr - xrb), yi = (float)(yr - yrb), zi = (float)(zr - zrb);
// Prime pre-multiplication for hash. Also flip seed for second lattice copy.
long xrbp = xrb * PRIME_X, yrbp = yrb * PRIME_Y, zrbp = zrb * PRIME_Z;
long seed2 = seed ^ -0x52D547B2E96ED629L;
// -1 if positive, 0 if negative.
int xNMask = (int)(-0.5f - xi), yNMask = (int)(-0.5f - yi), zNMask = (int)(-0.5f - zi);
// First vertex.
float x0 = xi + xNMask;
float y0 = yi + yNMask;
float z0 = zi + zNMask;
float a0 = RSQUARED_3D - x0 * x0 - y0 * y0 - z0 * z0;
float value = (a0 * a0) * (a0 * a0) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x0, y0, z0 );
// Second vertex.
float x1 = xi - 0.5f;
float y1 = yi - 0.5f;
float z1 = zi - 0.5f;
float a1 = RSQUARED_3D - x1 * x1 - y1 * y1 - z1 * z1;
value += (a1 * a1) * (a1 * a1) * Grad( seed2,
xrbp + PRIME_X, yrbp + PRIME_Y, zrbp + PRIME_Z, x1, y1, z1 );
// Shortcuts for building the remaining falloffs.
// Derived by subtracting the polynomials with the offsets plugged in.
float xAFlipMask0 = ((xNMask | 1) << 1) * x1;
float yAFlipMask0 = ((yNMask | 1) << 1) * y1;
float zAFlipMask0 = ((zNMask | 1) << 1) * z1;
float xAFlipMask1 = (-2 - (xNMask << 2)) * x1 - 1.0f;
float yAFlipMask1 = (-2 - (yNMask << 2)) * y1 - 1.0f;
float zAFlipMask1 = (-2 - (zNMask << 2)) * z1 - 1.0f;
bool skip5 = false;
float a2 = xAFlipMask0 + a0;
if ( a2 > 0 )
{
float x2 = x0 - (xNMask | 1);
float y2 = y0;
float z2 = z0;
value += (a2 * a2) * (a2 * a2) * Grad( seed,
xrbp + (~xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x2, y2, z2 );
}
else
{
float a3 = yAFlipMask0 + zAFlipMask0 + a0;
if ( a3 > 0 )
{
float x3 = x0;
float y3 = y0 - (yNMask | 1);
float z3 = z0 - (zNMask | 1);
value += (a3 * a3) * (a3 * a3) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), x3, y3, z3 );
}
float a4 = xAFlipMask1 + a1;
if ( a4 > 0 )
{
float x4 = (xNMask | 1) + x1;
float y4 = y1;
float z4 = z1;
value += (a4 * a4) * (a4 * a4) * Grad( seed2,
xrbp + (xNMask & unchecked(PRIME_X * 2)), yrbp + PRIME_Y, zrbp + PRIME_Z, x4, y4, z4 );
skip5 = true;
}
}
bool skip9 = false;
float a6 = yAFlipMask0 + a0;
if ( a6 > 0 )
{
float x6 = x0;
float y6 = y0 - (yNMask | 1);
float z6 = z0;
value += (a6 * a6) * (a6 * a6) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x6, y6, z6 );
}
else
{
float a7 = xAFlipMask0 + zAFlipMask0 + a0;
if ( a7 > 0 )
{
float x7 = x0 - (xNMask | 1);
float y7 = y0;
float z7 = z0 - (zNMask | 1);
value += (a7 * a7) * (a7 * a7) * Grad( seed,
xrbp + (~xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), x7, y7, z7 );
}
float a8 = yAFlipMask1 + a1;
if ( a8 > 0 )
{
float x8 = x1;
float y8 = (yNMask | 1) + y1;
float z8 = z1;
value += (a8 * a8) * (a8 * a8) * Grad( seed2,
xrbp + PRIME_X, yrbp + (yNMask & (PRIME_Y << 1)), zrbp + PRIME_Z, x8, y8, z8 );
skip9 = true;
}
}
bool skipD = false;
float aA = zAFlipMask0 + a0;
if ( aA > 0 )
{
float xA = x0;
float yA = y0;
float zA = z0 - (zNMask | 1);
value += (aA * aA) * (aA * aA) * Grad( seed,
xrbp + (xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), xA, yA, zA );
}
else
{
float aB = xAFlipMask0 + yAFlipMask0 + a0;
if ( aB > 0 )
{
float xB = x0 - (xNMask | 1);
float yB = y0 - (yNMask | 1);
float zB = z0;
value += (aB * aB) * (aB * aB) * Grad( seed,
xrbp + (~xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), xB, yB, zB );
}
float aC = zAFlipMask1 + a1;
if ( aC > 0 )
{
float xC = x1;
float yC = y1;
float zC = (zNMask | 1) + z1;
value += (aC * aC) * (aC * aC) * Grad( seed2,
xrbp + PRIME_X, yrbp + PRIME_Y, zrbp + (zNMask & (PRIME_Z << 1)), xC, yC, zC );
skipD = true;
}
}
if ( !skip5 )
{
float a5 = yAFlipMask1 + zAFlipMask1 + a1;
if ( a5 > 0 )
{
float x5 = x1;
float y5 = (yNMask | 1) + y1;
float z5 = (zNMask | 1) + z1;
value += (a5 * a5) * (a5 * a5) * Grad( seed2,
xrbp + PRIME_X, yrbp + (yNMask & (PRIME_Y << 1)), zrbp + (zNMask & (PRIME_Z << 1)), x5, y5, z5 );
}
}
if ( !skip9 )
{
float a9 = xAFlipMask1 + zAFlipMask1 + a1;
if ( a9 > 0 )
{
float x9 = (xNMask | 1) + x1;
float y9 = y1;
float z9 = (zNMask | 1) + z1;
value += (a9 * a9) * (a9 * a9) * Grad( seed2,
xrbp + (xNMask & unchecked(PRIME_X * 2)), yrbp + PRIME_Y, zrbp + (zNMask & (PRIME_Z << 1)), x9, y9, z9 );
}
}
if ( !skipD )
{
float aD = xAFlipMask1 + yAFlipMask1 + a1;
if ( aD > 0 )
{
float xD = (xNMask | 1) + x1;
float yD = (yNMask | 1) + y1;
float zD = z1;
value += (aD * aD) * (aD * aD) * Grad( seed2,
xrbp + (xNMask & (PRIME_X << 1)), yrbp + (yNMask & (PRIME_Y << 1)), zrbp + PRIME_Z, xD, yD, zD );
}
}
return value;
}
/**
* 4D SuperSimplex noise, with XYZ oriented like Noise3_ImproveXY
* and W for an extra degree of freedom. W repeats eventually.
* Recommended for time-varied animations which texture a 3D object (W=time)
* in a space where Z is vertical
*/
public static float Noise4_ImproveXYZ_ImproveXY( long seed, double x, double y, double z, double w )
{
double xy = x + y;
double s2 = xy * -0.21132486540518699998;
double zz = z * 0.28867513459481294226;
double ww = w * 1.118033988749894;
double xr = x + (zz + ww + s2), yr = y + (zz + ww + s2);
double zr = xy * -0.57735026918962599998 + (zz + ww);
double wr = z * -0.866025403784439 + ww;
return Noise4_UnskewedBase( seed, xr, yr, zr, wr );
}
/**
* 4D SuperSimplex noise, with XYZ oriented like Noise3_ImproveXZ
* and W for an extra degree of freedom. W repeats eventually.
* Recommended for time-varied animations which texture a 3D object (W=time)
* in a space where Y is vertical
*/
public static float Noise4_ImproveXYZ_ImproveXZ( long seed, double x, double y, double z, double w )
{
double xz = x + z;
double s2 = xz * -0.21132486540518699998;
double yy = y * 0.28867513459481294226;
double ww = w * 1.118033988749894;
double xr = x + (yy + ww + s2), zr = z + (yy + ww + s2);
double yr = xz * -0.57735026918962599998 + (yy + ww);
double wr = y * -0.866025403784439 + ww;
return Noise4_UnskewedBase( seed, xr, yr, zr, wr );
}
/**
* 4D SuperSimplex noise, with XYZ oriented like Noise3_Fallback
* and W for an extra degree of freedom. W repeats eventually.
* Recommended for time-varied animations which texture a 3D object (W=time)
* where there isn't a clear distinction between horizontal and vertical
*/
public static float Noise4_ImproveXYZ( long seed, double x, double y, double z, double w )
{
double xyz = x + y + z;
double ww = w * 1.118033988749894;
double s2 = xyz * -0.16666666666666666 + ww;
double xs = x + s2, ys = y + s2, zs = z + s2, ws = -0.5 * xyz + ww;
return Noise4_UnskewedBase( seed, xs, ys, zs, ws );
}
/**
* 4D SuperSimplex noise, fallback lattice orientation.
*/
public static float Noise4_Fallback( long seed, double x, double y, double z, double w )
{
// Get points for A4 lattice
double s = SKEW_4D * (x + y + z + w);
double xs = x + s, ys = y + s, zs = z + s, ws = w + s;
return Noise4_UnskewedBase( seed, xs, ys, zs, ws );
}
/**
* 4D SuperSimplex noise base.
* Using ultra-simple 4x4x4x4 lookup partitioning.
* This isn't as elegant or SIMD/GPU/etc. portable as other approaches,
* but it competes performance-wise with optimized 2014 OpenSimplex.
*/
private static float Noise4_UnskewedBase( long seed, double xs, double ys, double zs, double ws )
{
// Get base points and offsets
int xsb = FastFloor( xs ), ysb = FastFloor( ys ), zsb = FastFloor( zs ), wsb = FastFloor( ws );
float xsi = (float)(xs - xsb), ysi = (float)(ys - ysb), zsi = (float)(zs - zsb), wsi = (float)(ws - wsb);
// Unskewed offsets
float ssi = (xsi + ysi + zsi + wsi) * UNSKEW_4D;
float xi = xsi + ssi, yi = ysi + ssi, zi = zsi + ssi, wi = wsi + ssi;
// Prime pre-multiplication for hash.
long xsvp = xsb * PRIME_X, ysvp = ysb * PRIME_Y, zsvp = zsb * PRIME_Z, wsvp = wsb * PRIME_W;
// Index into initial table.
int index = ((FastFloor( xs * 4 ) & 3) << 0)
| ((FastFloor( ys * 4 ) & 3) << 2)
| ((FastFloor( zs * 4 ) & 3) << 4)
| ((FastFloor( ws * 4 ) & 3) << 6);
// Point contributions
float value = 0;
(int secondaryIndexStart, int secondaryIndexStop) = LOOKUP_4D_A[index];
for ( int i = secondaryIndexStart; i < secondaryIndexStop; i++ )
{
LatticeVertex4D c = LOOKUP_4D_B[i];
float dx = xi + c.dx, dy = yi + c.dy, dz = zi + c.dz, dw = wi + c.dw;
float a = (dx * dx + dy * dy) + (dz * dz + dw * dw);
if ( a < RSQUARED_4D )
{
a -= RSQUARED_4D;
a *= a;
value += a * a * Grad( seed, xsvp + c.xsvp, ysvp + c.ysvp, zsvp + c.zsvp, wsvp + c.wsvp, dx, dy, dz, dw );
}
}
return value;
}
/*
* Utility
*/
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static float Grad( long seed, long xsvp, long ysvp, float dx, float dy )
{
long hash = seed ^ xsvp ^ ysvp;
hash *= HASH_MULTIPLIER;
hash ^= hash >> (64 - N_GRADS_2D_EXPONENT + 1);
int gi = (int)hash & ((N_GRADS_2D - 1) << 1);
return GRADIENTS_2D[gi | 0] * dx + GRADIENTS_2D[gi | 1] * dy;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static float Grad( long seed, long xrvp, long yrvp, long zrvp, float dx, float dy, float dz )
{
long hash = (seed ^ xrvp) ^ (yrvp ^ zrvp);
hash *= HASH_MULTIPLIER;
hash ^= hash >> (64 - N_GRADS_3D_EXPONENT + 2);
int gi = (int)hash & ((N_GRADS_3D - 1) << 2);
return GRADIENTS_3D[gi | 0] * dx + GRADIENTS_3D[gi | 1] * dy + GRADIENTS_3D[gi | 2] * dz;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static float Grad( long seed, long xsvp, long ysvp, long zsvp, long wsvp, float dx, float dy, float dz, float dw )
{
long hash = seed ^ (xsvp ^ ysvp) ^ (zsvp ^ wsvp);
hash *= HASH_MULTIPLIER;
hash ^= hash >> (64 - N_GRADS_4D_EXPONENT + 2);
int gi = (int)hash & ((N_GRADS_4D - 1) << 2);
return (GRADIENTS_4D[gi | 0] * dx + GRADIENTS_4D[gi | 1] * dy) + (GRADIENTS_4D[gi | 2] * dz + GRADIENTS_4D[gi | 3] * dw);
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static int FastFloor( double x )
{
int xi = (int)x;
return x < xi ? xi - 1 : xi;
}
/*
* Lookup Tables & Gradients
*/
private static readonly float[] GRADIENTS_2D;
private static readonly float[] GRADIENTS_3D;
private static readonly float[] GRADIENTS_4D;
private static readonly (short SecondaryIndexStart, short SecondaryIndexStop)[] LOOKUP_4D_A;
private static readonly LatticeVertex4D[] LOOKUP_4D_B;
static OpenSimplex2S()
{
GRADIENTS_2D = new float[N_GRADS_2D * 2];
float[] grad2 = {
0.38268343236509f, 0.923879532511287f,
0.923879532511287f, 0.38268343236509f,
0.923879532511287f, -0.38268343236509f,
0.38268343236509f, -0.923879532511287f,
-0.38268343236509f, -0.923879532511287f,
-0.923879532511287f, -0.38268343236509f,
-0.923879532511287f, 0.38268343236509f,
-0.38268343236509f, 0.923879532511287f,
//-------------------------------------//
0.130526192220052f, 0.99144486137381f,
0.608761429008721f, 0.793353340291235f,
0.793353340291235f, 0.608761429008721f,
0.99144486137381f, 0.130526192220051f,
0.99144486137381f, -0.130526192220051f,
0.793353340291235f, -0.60876142900872f,
0.608761429008721f, -0.793353340291235f,
0.130526192220052f, -0.99144486137381f,
-0.130526192220052f, -0.99144486137381f,
-0.608761429008721f, -0.793353340291235f,
-0.793353340291235f, -0.608761429008721f,
-0.99144486137381f, -0.130526192220052f,
-0.99144486137381f, 0.130526192220051f,
-0.793353340291235f, 0.608761429008721f,
-0.608761429008721f, 0.793353340291235f,
-0.130526192220052f, 0.99144486137381f,
};
for ( int i = 0; i < grad2.Length; i++ )
{
grad2[i] = (float)(grad2[i] / NORMALIZER_2D);
}
for ( int i = 0, j = 0; i < GRADIENTS_2D.Length; i++, j++ )
{
if ( j == grad2.Length ) j = 0;
GRADIENTS_2D[i] = grad2[j];
}
GRADIENTS_3D = new float[N_GRADS_3D * 4];
float[] grad3 = {
2.22474487139f, 2.22474487139f, -1.0f, 0.0f,
2.22474487139f, 2.22474487139f, 1.0f, 0.0f,
3.0862664687972017f, 1.1721513422464978f, 0.0f, 0.0f,
1.1721513422464978f, 3.0862664687972017f, 0.0f, 0.0f,
-2.22474487139f, 2.22474487139f, -1.0f, 0.0f,
-2.22474487139f, 2.22474487139f, 1.0f, 0.0f,
-1.1721513422464978f, 3.0862664687972017f, 0.0f, 0.0f,
-3.0862664687972017f, 1.1721513422464978f, 0.0f, 0.0f,
-1.0f, -2.22474487139f, -2.22474487139f, 0.0f,
1.0f, -2.22474487139f, -2.22474487139f, 0.0f,
0.0f, -3.0862664687972017f, -1.1721513422464978f, 0.0f,
0.0f, -1.1721513422464978f, -3.0862664687972017f, 0.0f,
-1.0f, -2.22474487139f, 2.22474487139f, 0.0f,
1.0f, -2.22474487139f, 2.22474487139f, 0.0f,
0.0f, -1.1721513422464978f, 3.0862664687972017f, 0.0f,
0.0f, -3.0862664687972017f, 1.1721513422464978f, 0.0f,
//--------------------------------------------------------------------//
-2.22474487139f, -2.22474487139f, -1.0f, 0.0f,
-2.22474487139f, -2.22474487139f, 1.0f, 0.0f,
-3.0862664687972017f, -1.1721513422464978f, 0.0f, 0.0f,
-1.1721513422464978f, -3.0862664687972017f, 0.0f, 0.0f,
-2.22474487139f, -1.0f, -2.22474487139f, 0.0f,
-2.22474487139f, 1.0f, -2.22474487139f, 0.0f,
-1.1721513422464978f, 0.0f, -3.0862664687972017f, 0.0f,
-3.0862664687972017f, 0.0f, -1.1721513422464978f, 0.0f,
-2.22474487139f, -1.0f, 2.22474487139f, 0.0f,
-2.22474487139f, 1.0f, 2.22474487139f, 0.0f,
-3.0862664687972017f, 0.0f, 1.1721513422464978f, 0.0f,
-1.1721513422464978f, 0.0f, 3.0862664687972017f, 0.0f,
-1.0f, 2.22474487139f, -2.22474487139f, 0.0f,
1.0f, 2.22474487139f, -2.22474487139f, 0.0f,
0.0f, 1.1721513422464978f, -3.0862664687972017f, 0.0f,
0.0f, 3.0862664687972017f, -1.1721513422464978f, 0.0f,
-1.0f, 2.22474487139f, 2.22474487139f, 0.0f,
1.0f, 2.22474487139f, 2.22474487139f, 0.0f,
0.0f, 3.0862664687972017f, 1.1721513422464978f, 0.0f,
0.0f, 1.1721513422464978f, 3.0862664687972017f, 0.0f,
2.22474487139f, -2.22474487139f, -1.0f, 0.0f,
2.22474487139f, -2.22474487139f, 1.0f, 0.0f,
1.1721513422464978f, -3.0862664687972017f, 0.0f, 0.0f,
3.0862664687972017f, -1.1721513422464978f, 0.0f, 0.0f,
2.22474487139f, -1.0f, -2.22474487139f, 0.0f,
2.22474487139f, 1.0f, -2.22474487139f, 0.0f,
3.0862664687972017f, 0.0f, -1.1721513422464978f, 0.0f,
1.1721513422464978f, 0.0f, -3.0862664687972017f, 0.0f,
2.22474487139f, -1.0f, 2.22474487139f, 0.0f,
2.22474487139f, 1.0f, 2.22474487139f, 0.0f,
1.1721513422464978f, 0.0f, 3.0862664687972017f, 0.0f,
3.0862664687972017f, 0.0f, 1.1721513422464978f, 0.0f,
};
for ( int i = 0; i < grad3.Length; i++ )
{
grad3[i] = (float)(grad3[i] / NORMALIZER_3D);
}
for ( int i = 0, j = 0; i < GRADIENTS_3D.Length; i++, j++ )
{
if ( j == grad3.Length ) j = 0;
GRADIENTS_3D[i] = grad3[j];
}
GRADIENTS_4D = new float[N_GRADS_4D * 4];
float[] grad4 = {
-0.6740059517812944f, -0.3239847771997537f, -0.3239847771997537f, 0.5794684678643381f,
-0.7504883828755602f, -0.4004672082940195f, 0.15296486218853164f, 0.5029860367700724f,
-0.7504883828755602f, 0.15296486218853164f, -0.4004672082940195f, 0.5029860367700724f,
-0.8828161875373585f, 0.08164729285680945f, 0.08164729285680945f, 0.4553054119602712f,
-0.4553054119602712f, -0.08164729285680945f, -0.08164729285680945f, 0.8828161875373585f,
-0.5029860367700724f, -0.15296486218853164f, 0.4004672082940195f, 0.7504883828755602f,
-0.5029860367700724f, 0.4004672082940195f, -0.15296486218853164f, 0.7504883828755602f,
-0.5794684678643381f, 0.3239847771997537f, 0.3239847771997537f, 0.6740059517812944f,
-0.6740059517812944f, -0.3239847771997537f, 0.5794684678643381f, -0.3239847771997537f,
-0.7504883828755602f, -0.4004672082940195f, 0.5029860367700724f, 0.15296486218853164f,
-0.7504883828755602f, 0.15296486218853164f, 0.5029860367700724f, -0.4004672082940195f,
-0.8828161875373585f, 0.08164729285680945f, 0.4553054119602712f, 0.08164729285680945f,
-0.4553054119602712f, -0.08164729285680945f, 0.8828161875373585f, -0.08164729285680945f,
-0.5029860367700724f, -0.15296486218853164f, 0.7504883828755602f, 0.4004672082940195f,
-0.5029860367700724f, 0.4004672082940195f, 0.7504883828755602f, -0.15296486218853164f,
-0.5794684678643381f, 0.3239847771997537f, 0.6740059517812944f, 0.3239847771997537f,
-0.6740059517812944f, 0.5794684678643381f, -0.3239847771997537f, -0.3239847771997537f,
-0.7504883828755602f, 0.5029860367700724f, -0.4004672082940195f, 0.15296486218853164f,
-0.7504883828755602f, 0.5029860367700724f, 0.15296486218853164f, -0.4004672082940195f,
-0.8828161875373585f, 0.4553054119602712f, 0.08164729285680945f, 0.08164729285680945f,
-0.4553054119602712f, 0.8828161875373585f, -0.08164729285680945f, -0.08164729285680945f,
-0.5029860367700724f, 0.7504883828755602f, -0.15296486218853164f, 0.4004672082940195f,
-0.5029860367700724f, 0.7504883828755602f, 0.4004672082940195f, -0.15296486218853164f,
-0.5794684678643381f, 0.6740059517812944f, 0.3239847771997537f, 0.3239847771997537f,
0.5794684678643381f, -0.6740059517812944f, -0.3239847771997537f, -0.3239847771997537f,
0.5029860367700724f, -0.7504883828755602f, -0.4004672082940195f, 0.15296486218853164f,
0.5029860367700724f, -0.7504883828755602f, 0.15296486218853164f, -0.4004672082940195f,
0.4553054119602712f, -0.8828161875373585f, 0.08164729285680945f, 0.08164729285680945f,
0.8828161875373585f, -0.4553054119602712f, -0.08164729285680945f, -0.08164729285680945f,
0.7504883828755602f, -0.5029860367700724f, -0.15296486218853164f, 0.4004672082940195f,
0.7504883828755602f, -0.5029860367700724f, 0.4004672082940195f, -0.15296486218853164f,
0.6740059517812944f, -0.5794684678643381f, 0.3239847771997537f, 0.3239847771997537f,
//------------------------------------------------------------------------------------------//
-0.753341017856078f, -0.37968289875261624f, -0.37968289875261624f, -0.37968289875261624f,
-0.7821684431180708f, -0.4321472685365301f, -0.4321472685365301f, 0.12128480194602098f,
-0.7821684431180708f, -0.4321472685365301f, 0.12128480194602098f, -0.4321472685365301f,
-0.7821684431180708f, 0.12128480194602098f, -0.4321472685365301f, -0.4321472685365301f,
-0.8586508742123365f, -0.508629699630796f, 0.044802370851755174f, 0.044802370851755174f,
-0.8586508742123365f, 0.044802370851755174f, -0.508629699630796f, 0.044802370851755174f,
-0.8586508742123365f, 0.044802370851755174f, 0.044802370851755174f, -0.508629699630796f,
-0.9982828964265062f, -0.03381941603233842f, -0.03381941603233842f, -0.03381941603233842f,
-0.37968289875261624f, -0.753341017856078f, -0.37968289875261624f, -0.37968289875261624f,
-0.4321472685365301f, -0.7821684431180708f, -0.4321472685365301f, 0.12128480194602098f,
-0.4321472685365301f, -0.7821684431180708f, 0.12128480194602098f, -0.4321472685365301f,
0.12128480194602098f, -0.7821684431180708f, -0.4321472685365301f, -0.4321472685365301f,
-0.508629699630796f, -0.8586508742123365f, 0.044802370851755174f, 0.044802370851755174f,
0.044802370851755174f, -0.8586508742123365f, -0.508629699630796f, 0.044802370851755174f,
0.044802370851755174f, -0.8586508742123365f, 0.044802370851755174f, -0.508629699630796f,
-0.03381941603233842f, -0.9982828964265062f, -0.03381941603233842f, -0.03381941603233842f,
-0.37968289875261624f, -0.37968289875261624f, -0.753341017856078f, -0.37968289875261624f,
-0.4321472685365301f, -0.4321472685365301f, -0.7821684431180708f, 0.12128480194602098f,
-0.4321472685365301f, 0.12128480194602098f, -0.7821684431180708f, -0.4321472685365301f,
0.12128480194602098f, -0.4321472685365301f, -0.7821684431180708f, -0.4321472685365301f,
-0.508629699630796f, 0.044802370851755174f, -0.8586508742123365f, 0.044802370851755174f,
0.044802370851755174f, -0.508629699630796f, -0.8586508742123365f, 0.044802370851755174f,
0.044802370851755174f, 0.044802370851755174f, -0.8586508742123365f, -0.508629699630796f,
-0.03381941603233842f, -0.03381941603233842f, -0.9982828964265062f, -0.03381941603233842f,
-0.37968289875261624f, -0.37968289875261624f, -0.37968289875261624f, -0.753341017856078f,
-0.4321472685365301f, -0.4321472685365301f, 0.12128480194602098f, -0.7821684431180708f,
-0.4321472685365301f, 0.12128480194602098f, -0.4321472685365301f, -0.7821684431180708f,
0.12128480194602098f, -0.4321472685365301f, -0.4321472685365301f, -0.7821684431180708f,
-0.508629699630796f, 0.044802370851755174f, 0.044802370851755174f, -0.8586508742123365f,
0.044802370851755174f, -0.508629699630796f, 0.044802370851755174f, -0.8586508742123365f,
0.044802370851755174f, 0.044802370851755174f, -0.508629699630796f, -0.8586508742123365f,
-0.03381941603233842f, -0.03381941603233842f, -0.03381941603233842f, -0.9982828964265062f,
-0.3239847771997537f, -0.6740059517812944f, -0.3239847771997537f, 0.5794684678643381f,
-0.4004672082940195f, -0.7504883828755602f, 0.15296486218853164f, 0.5029860367700724f,
0.15296486218853164f, -0.7504883828755602f, -0.4004672082940195f, 0.5029860367700724f,
0.08164729285680945f, -0.8828161875373585f, 0.08164729285680945f, 0.4553054119602712f,
-0.08164729285680945f, -0.4553054119602712f, -0.08164729285680945f, 0.8828161875373585f,
-0.15296486218853164f, -0.5029860367700724f, 0.4004672082940195f, 0.7504883828755602f,
0.4004672082940195f, -0.5029860367700724f, -0.15296486218853164f, 0.7504883828755602f,
0.3239847771997537f, -0.5794684678643381f, 0.3239847771997537f, 0.6740059517812944f,
-0.3239847771997537f, -0.3239847771997537f, -0.6740059517812944f, 0.5794684678643381f,
-0.4004672082940195f, 0.15296486218853164f, -0.7504883828755602f, 0.5029860367700724f,
0.15296486218853164f, -0.4004672082940195f, -0.7504883828755602f, 0.5029860367700724f,
0.08164729285680945f, 0.08164729285680945f, -0.8828161875373585f, 0.4553054119602712f,
-0.08164729285680945f, -0.08164729285680945f, -0.4553054119602712f, 0.8828161875373585f,
-0.15296486218853164f, 0.4004672082940195f, -0.5029860367700724f, 0.7504883828755602f,
0.4004672082940195f, -0.15296486218853164f, -0.5029860367700724f, 0.7504883828755602f,
0.3239847771997537f, 0.3239847771997537f, -0.5794684678643381f, 0.6740059517812944f,
-0.3239847771997537f, -0.6740059517812944f, 0.5794684678643381f, -0.3239847771997537f,
-0.4004672082940195f, -0.7504883828755602f, 0.5029860367700724f, 0.15296486218853164f,
0.15296486218853164f, -0.7504883828755602f, 0.5029860367700724f, -0.4004672082940195f,
0.08164729285680945f, -0.8828161875373585f, 0.4553054119602712f, 0.08164729285680945f,
-0.08164729285680945f, -0.4553054119602712f, 0.8828161875373585f, -0.08164729285680945f,
-0.15296486218853164f, -0.5029860367700724f, 0.7504883828755602f, 0.4004672082940195f,
0.4004672082940195f, -0.5029860367700724f, 0.7504883828755602f, -0.15296486218853164f,
0.3239847771997537f, -0.5794684678643381f, 0.6740059517812944f, 0.3239847771997537f,
-0.3239847771997537f, -0.3239847771997537f, 0.5794684678643381f, -0.6740059517812944f,
-0.4004672082940195f, 0.15296486218853164f, 0.5029860367700724f, -0.7504883828755602f,
0.15296486218853164f, -0.4004672082940195f, 0.5029860367700724f, -0.7504883828755602f,
0.08164729285680945f, 0.08164729285680945f, 0.4553054119602712f, -0.8828161875373585f,
-0.08164729285680945f, -0.08164729285680945f, 0.8828161875373585f, -0.4553054119602712f,
-0.15296486218853164f, 0.4004672082940195f, 0.7504883828755602f, -0.5029860367700724f,
0.4004672082940195f, -0.15296486218853164f, 0.7504883828755602f, -0.5029860367700724f,
0.3239847771997537f, 0.3239847771997537f, 0.6740059517812944f, -0.5794684678643381f,
-0.3239847771997537f, 0.5794684678643381f, -0.6740059517812944f, -0.3239847771997537f,
-0.4004672082940195f, 0.5029860367700724f, -0.7504883828755602f, 0.15296486218853164f,
0.15296486218853164f, 0.5029860367700724f, -0.7504883828755602f, -0.4004672082940195f,
0.08164729285680945f, 0.4553054119602712f, -0.8828161875373585f, 0.08164729285680945f,
-0.08164729285680945f, 0.8828161875373585f, -0.4553054119602712f, -0.08164729285680945f,
-0.15296486218853164f, 0.7504883828755602f, -0.5029860367700724f, 0.4004672082940195f,
0.4004672082940195f, 0.7504883828755602f, -0.5029860367700724f, -0.15296486218853164f,
0.3239847771997537f, 0.6740059517812944f, -0.5794684678643381f, 0.3239847771997537f,
-0.3239847771997537f, 0.5794684678643381f, -0.3239847771997537f, -0.6740059517812944f,
-0.4004672082940195f, 0.5029860367700724f, 0.15296486218853164f, -0.7504883828755602f,
0.15296486218853164f, 0.5029860367700724f, -0.4004672082940195f, -0.7504883828755602f,
0.08164729285680945f, 0.4553054119602712f, 0.08164729285680945f, -0.8828161875373585f,
-0.08164729285680945f, 0.8828161875373585f, -0.08164729285680945f, -0.4553054119602712f,
-0.15296486218853164f, 0.7504883828755602f, 0.4004672082940195f, -0.5029860367700724f,
0.4004672082940195f, 0.7504883828755602f, -0.15296486218853164f, -0.5029860367700724f,
0.3239847771997537f, 0.6740059517812944f, 0.3239847771997537f, -0.5794684678643381f,
0.5794684678643381f, -0.3239847771997537f, -0.6740059517812944f, -0.3239847771997537f,
0.5029860367700724f, -0.4004672082940195f, -0.7504883828755602f, 0.15296486218853164f,
0.5029860367700724f, 0.15296486218853164f, -0.7504883828755602f, -0.4004672082940195f,
0.4553054119602712f, 0.08164729285680945f, -0.8828161875373585f, 0.08164729285680945f,
0.8828161875373585f, -0.08164729285680945f, -0.4553054119602712f, -0.08164729285680945f,
0.7504883828755602f, -0.15296486218853164f, -0.5029860367700724f, 0.4004672082940195f,
0.7504883828755602f, 0.4004672082940195f, -0.5029860367700724f, -0.15296486218853164f,
0.6740059517812944f, 0.3239847771997537f, -0.5794684678643381f, 0.3239847771997537f,
0.5794684678643381f, -0.3239847771997537f, -0.3239847771997537f, -0.6740059517812944f,
0.5029860367700724f, -0.4004672082940195f, 0.15296486218853164f, -0.7504883828755602f,
0.5029860367700724f, 0.15296486218853164f, -0.4004672082940195f, -0.7504883828755602f,
0.4553054119602712f, 0.08164729285680945f, 0.08164729285680945f, -0.8828161875373585f,
0.8828161875373585f, -0.08164729285680945f, -0.08164729285680945f, -0.4553054119602712f,
0.7504883828755602f, -0.15296486218853164f, 0.4004672082940195f, -0.5029860367700724f,
0.7504883828755602f, 0.4004672082940195f, -0.15296486218853164f, -0.5029860367700724f,
0.6740059517812944f, 0.3239847771997537f, 0.3239847771997537f, -0.5794684678643381f,
0.03381941603233842f, 0.03381941603233842f, 0.03381941603233842f, 0.9982828964265062f,
-0.044802370851755174f, -0.044802370851755174f, 0.508629699630796f, 0.8586508742123365f,
-0.044802370851755174f, 0.508629699630796f, -0.044802370851755174f, 0.8586508742123365f,
-0.12128480194602098f, 0.4321472685365301f, 0.4321472685365301f, 0.7821684431180708f,
0.508629699630796f, -0.044802370851755174f, -0.044802370851755174f, 0.8586508742123365f,
0.4321472685365301f, -0.12128480194602098f, 0.4321472685365301f, 0.7821684431180708f,
0.4321472685365301f, 0.4321472685365301f, -0.12128480194602098f, 0.7821684431180708f,
0.37968289875261624f, 0.37968289875261624f, 0.37968289875261624f, 0.753341017856078f,
0.03381941603233842f, 0.03381941603233842f, 0.9982828964265062f, 0.03381941603233842f,
-0.044802370851755174f, 0.044802370851755174f, 0.8586508742123365f, 0.508629699630796f,
-0.044802370851755174f, 0.508629699630796f, 0.8586508742123365f, -0.044802370851755174f,
-0.12128480194602098f, 0.4321472685365301f, 0.7821684431180708f, 0.4321472685365301f,
0.508629699630796f, -0.044802370851755174f, 0.8586508742123365f, -0.044802370851755174f,
0.4321472685365301f, -0.12128480194602098f, 0.7821684431180708f, 0.4321472685365301f,
0.4321472685365301f, 0.4321472685365301f, 0.7821684431180708f, -0.12128480194602098f,
0.37968289875261624f, 0.37968289875261624f, 0.753341017856078f, 0.37968289875261624f,
0.03381941603233842f, 0.9982828964265062f, 0.03381941603233842f, 0.03381941603233842f,
-0.044802370851755174f, 0.8586508742123365f, -0.044802370851755174f, 0.508629699630796f,
-0.044802370851755174f, 0.8586508742123365f, 0.508629699630796f, -0.044802370851755174f,
-0.12128480194602098f, 0.7821684431180708f, 0.4321472685365301f, 0.4321472685365301f,
0.508629699630796f, 0.8586508742123365f, -0.044802370851755174f, -0.044802370851755174f,
0.4321472685365301f, 0.7821684431180708f, -0.12128480194602098f, 0.4321472685365301f,
0.4321472685365301f, 0.7821684431180708f, 0.4321472685365301f, -0.12128480194602098f,
0.37968289875261624f, 0.753341017856078f, 0.37968289875261624f, 0.37968289875261624f,
0.9982828964265062f, 0.03381941603233842f, 0.03381941603233842f, 0.03381941603233842f,
0.8586508742123365f, -0.044802370851755174f, -0.044802370851755174f, 0.508629699630796f,
0.8586508742123365f, -0.044802370851755174f, 0.508629699630796f, -0.044802370851755174f,
0.7821684431180708f, -0.12128480194602098f, 0.4321472685365301f, 0.4321472685365301f,
0.8586508742123365f, 0.508629699630796f, -0.044802370851755174f, -0.044802370851755174f,
0.7821684431180708f, 0.4321472685365301f, -0.12128480194602098f, 0.4321472685365301f,
0.7821684431180708f, 0.4321472685365301f, 0.4321472685365301f, -0.12128480194602098f,
0.753341017856078f, 0.37968289875261624f, 0.37968289875261624f, 0.37968289875261624f,
};
for ( int i = 0; i < grad4.Length; i++ )
{
grad4[i] = (float)(grad4[i] / NORMALIZER_4D);
}
for ( int i = 0, j = 0; i < GRADIENTS_4D.Length; i++, j++ )
{
if ( j == grad4.Length ) j = 0;
GRADIENTS_4D[i] = grad4[j];
}
int[][] lookup4DVertexCodes = {
new int[] { 0x15, 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x01, 0x05, 0x11, 0x15, 0x41, 0x45, 0x51, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x01, 0x15, 0x16, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x15, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x16, 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xAA, 0xAB },
new int[] { 0x04, 0x05, 0x14, 0x15, 0x44, 0x45, 0x54, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0xAA },
new int[] { 0x05, 0x15, 0x16, 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x5B, 0x6A, 0x9A, 0xAA, 0xAB },
new int[] { 0x04, 0x15, 0x19, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x05, 0x15, 0x19, 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xAA, 0xAE },
new int[] { 0x05, 0x15, 0x19, 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x5E, 0x6A, 0x9A, 0xAA, 0xAE },
new int[] { 0x05, 0x15, 0x1A, 0x45, 0x4A, 0x55, 0x56, 0x59, 0x5A, 0x5B, 0x5E, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x15, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x16, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x6B, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xAA },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x6B, 0x9A, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x19, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x6E, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x6E, 0x9A, 0xAA, 0xAE },
new int[] { 0x15, 0x1A, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x6B, 0x6E, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x10, 0x11, 0x14, 0x15, 0x50, 0x51, 0x54, 0x55, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0xA6, 0xAA },
new int[] { 0x11, 0x15, 0x16, 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x67, 0x6A, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA6, 0xAA },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x6B, 0xA6, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xAA },
new int[] { 0x15, 0x16, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0xAA, 0xAB },
new int[] { 0x14, 0x15, 0x19, 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x6A, 0x6D, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x6E, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x19, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6E, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x6B, 0x6E, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x10, 0x15, 0x25, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x11, 0x15, 0x25, 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xAA, 0xBA },
new int[] { 0x11, 0x15, 0x25, 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x6A, 0x76, 0xA6, 0xAA, 0xBA },
new int[] { 0x11, 0x15, 0x26, 0x51, 0x55, 0x56, 0x62, 0x65, 0x66, 0x67, 0x6A, 0x76, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x14, 0x15, 0x25, 0x54, 0x55, 0x59, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA6, 0xAA, 0xBA },
new int[] { 0x15, 0x26, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x6B, 0x7A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x14, 0x15, 0x25, 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x6A, 0x79, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x25, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0x7A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x14, 0x15, 0x29, 0x54, 0x55, 0x59, 0x65, 0x68, 0x69, 0x6A, 0x6D, 0x79, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x29, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x6E, 0x7A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6E, 0x7A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0x6E, 0x7A, 0xAA, 0xAB, 0xAE, 0xBA, 0xBF },
new int[] { 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x5A, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x5A, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xAA },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x9A, 0x9B, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x99, 0x9A, 0x9E, 0xAA, 0xAE },
new int[] { 0x45, 0x4A, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x59, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x56, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x66, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xAA },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA },
new int[] { 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA9, 0xAA },
new int[] { 0x15, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x66, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x61, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x6A, 0xA5, 0xA6, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x62, 0x65, 0x66, 0x6A, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x6A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x6A, 0xA5, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x15, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x68, 0x69, 0x6A, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xBE },
new int[] { 0x55, 0x59, 0x65, 0x69, 0x6A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x15, 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xAA, 0xAB, 0xAE, 0xBA, 0xBF },
new int[] { 0x40, 0x41, 0x44, 0x45, 0x50, 0x51, 0x54, 0x55, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x95, 0x96, 0x9A, 0xA6, 0xAA },
new int[] { 0x41, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x95, 0x96, 0x97, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x95, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xAA },
new int[] { 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0xAA, 0xAB },
new int[] { 0x44, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x95, 0x99, 0x9A, 0x9D, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x59, 0x5A, 0x95, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9E, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x99, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xEA },
new int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },
new int[] { 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x99, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA },
new int[] { 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB },
new int[] { 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE },
new int[] { 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xAF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB5, 0xBA },
new int[] { 0x51, 0x55, 0x61, 0x65, 0x66, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xB6, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x64, 0x65, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA5, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xB9, 0xBA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },
new int[] { 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA, 0xBB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xBE },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA5, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xBA },
new int[] { 0x40, 0x45, 0x51, 0x54, 0x55, 0x85, 0x91, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x85, 0x91, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA, 0xEA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x85, 0x91, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xD6, 0xEA },
new int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x86, 0x92, 0x95, 0x96, 0x97, 0x9A, 0xA6, 0xAA, 0xAB, 0xD6, 0xEA, 0xEB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x85, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x86, 0x95, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB, 0xDA, 0xEA, 0xEB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x85, 0x94, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xD9, 0xEA },
new int[] { 0x45, 0x55, 0x59, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xAA, 0xDA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB, 0xDA, 0xEA, 0xEB },
new int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x89, 0x95, 0x98, 0x99, 0x9A, 0x9D, 0xA9, 0xAA, 0xAE, 0xD9, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x59, 0x89, 0x95, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE, 0xDA, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE, 0xDA, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xDA, 0xEA, 0xEF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x91, 0x94, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x91, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x91, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x92, 0x95, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB, 0xE6, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x55, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x94, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x55, 0x59, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x45, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x95, 0x98, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE, 0xE9, 0xEA, 0xEE },
new int[] { 0x55, 0x59, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x45, 0x55, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xAA, 0xAB, 0xAE, 0xEA, 0xEF },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x91, 0x94, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xE5, 0xEA },
new int[] { 0x51, 0x55, 0x65, 0x91, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x91, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xE6, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xE6, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x65, 0x94, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x55, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x51, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x94, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xE9, 0xEA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x59, 0x65, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },
new int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xEA, 0xEB },
new int[] { 0x54, 0x55, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xE9, 0xEA, 0xEE },
new int[] { 0x54, 0x55, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xEA },
new int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0xA1, 0xA4, 0xA5, 0xA6, 0xA9, 0xAA, 0xB5, 0xBA, 0xE5, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x65, 0x95, 0xA1, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA, 0xE6, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA, 0xE6, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xE6, 0xEA, 0xFB },
new int[] { 0x54, 0x55, 0x65, 0x95, 0xA4, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA, 0xE9, 0xEA, 0xFA },
new int[] { 0x55, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x51, 0x55, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xAB, 0xBA, 0xEA, 0xFB },
new int[] { 0x54, 0x55, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA, 0xE9, 0xEA, 0xFA },
new int[] { 0x54, 0x55, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },
new int[] { 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA, 0xEA },
new int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xE9, 0xEA, 0xFE },
new int[] { 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAE, 0xBA, 0xEA, 0xFE },
new int[] { 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA, 0xEA },
new int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xBA, 0xEA },
};
LatticeVertex4D[] latticeVerticesByCode = new LatticeVertex4D[256];
for ( int i = 0; i < 256; i++ )
{
int cx = ((i >> 0) & 3) - 1;
int cy = ((i >> 2) & 3) - 1;
int cz = ((i >> 4) & 3) - 1;
int cw = ((i >> 6) & 3) - 1;
latticeVerticesByCode[i] = new LatticeVertex4D( cx, cy, cz, cw );
}
int nLatticeVerticesTotal = 0;
for ( int i = 0; i < 256; i++ )
{
nLatticeVerticesTotal += lookup4DVertexCodes[i].Length;
}
LOOKUP_4D_A = new (short SecondaryIndexStart, short SecondaryIndexStop)[256];
LOOKUP_4D_B = new LatticeVertex4D[nLatticeVerticesTotal];
for ( int i = 0, j = 0; i < 256; i++ )
{
LOOKUP_4D_A[i] = ((short)j, (short)(j + lookup4DVertexCodes[i].Length));
for ( int k = 0; k < lookup4DVertexCodes[i].Length; k++ )
{
LOOKUP_4D_B[j++] = latticeVerticesByCode[lookup4DVertexCodes[i][k]];
}
}
}
private class LatticeVertex4D
{
public readonly float dx, dy, dz, dw;
public readonly long xsvp, ysvp, zsvp, wsvp;
public LatticeVertex4D( int xsv, int ysv, int zsv, int wsv )
{
this.xsvp = xsv * PRIME_X; this.ysvp = ysv * PRIME_Y;
this.zsvp = zsv * PRIME_Z; this.wsvp = wsv * PRIME_W;
float ssv = (xsv + ysv + zsv + wsv) * UNSKEW_4D;
this.dx = -xsv - ssv;
this.dy = -ysv - ssv;
this.dz = -zsv - ssv;
this.dw = -wsv - ssv;
}
}
}
Editor
library
using Editor;
using Sandbox;
using System;
namespace Sturnus.TerrainGenerationTool;
public static class Islands
{
public static float Default( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize = 0.1f, float warpStrength = 0.5f )
{
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float warpX;
float warpY;
float warpedNx;
float warpedNy;
float noise;
if ( warp )
{
// Generate warp offsets using additional noise
warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
// Apply domain warping
warpedNx = nx + warpX;
warpedNy = ny + warpY;
}
else
{
warpedNx = nx;
warpedNy = ny;
}
// Radial distance from the center
float distance = (float)Math.Sqrt( nx * nx + ny * ny );
float falloff = 1.0f - Math.Clamp( distance, 0.25f, 1 ); // Smooth taper from center to edge
// Central mountain shape (parabolic for smooth curvature)
float centralMountain = (1.0f - distance * distance) * falloff;
// Beach-style taper near the edges
float beachStart = 0.5f; // Start of the beach region (distance normalized)
float beachEnd = 0.98f; // End of the beach region (ocean level)
float beachFalloff = Math.Clamp( (distance - beachStart) / (beachEnd - beachStart), 0.1f, 1 );
float beachTaper = (1.0f - beachFalloff) * 0.25f; // Smooth transition to flat region
// Add subtle noise for terrain variation
if ( warp )
{
noise = OpenSimplex2S.Noise2( seed, warpedNx * 2, warpedNy * 2 ) * 0.4f;
}
else
{
noise = OpenSimplex2S.Noise2( seed, nx * 6, ny * 6 ) * 0.05f; // Low-frequency noise
}
// Combine components: central mountain, beach taper, and noise
float output = centralMountain * (1.0f - beachFalloff) + beachTaper + noise;
// Combine all effects
float heightValue = output;
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Archipelagos( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1;
float ny = (y / (float)height) * 2 - 1;
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Use noise layers to create clusters of small islands
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.5f, ny * 1.5f );
float secondaryNoise = OpenSimplex2S.Noise2( seed + 1, nx * 3.0f, ny * 3.0f ) * 0.5f;
float archipelagoHeight = baseNoise + secondaryNoise;
// Apply radial falloff to form rounded island clusters
float distance = MathF.Sqrt( nx * nx + ny * ny );
float falloff = Math.Clamp( 1 - distance * 1.2f, 0, 1 );
float heightValue = Math.Clamp( archipelagoHeight * falloff, 0, 1 );
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Atoll( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 20, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 21, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Calculate distance from the center
float distance = MathF.Sqrt( nx * nx + ny * ny );
// Define parameters for the single ring
float ringCenter = 0.6f; // Center of the ring
float ringWidth = 0.1f; // Width of the ring
// Create a single ring using a Gaussian-like function
float ring = MathF.Exp( -MathF.Pow( (distance - ringCenter) / ringWidth, 2 ) );
// Add some noise for variation
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.0f, ny * 1.0f ) * 0.4f;
// Introduce a beach-like area (reduce noise and height for one side of the map)
float beachEffect = Math.Clamp( (1 - nx) * 0.5f, 0.2f, 1.0f ); // Reduces height on one side of the map
float beachNoise = OpenSimplex2S.Noise2( seed + 30, nx * 2.0f, ny * 2.0f ) * 0.2f;
// Combine the ring, noise, and beach effect
float heightValue = (ring + baseNoise * beachEffect + beachNoise) * beachEffect;
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Islets( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1;
float ny = (y / (float)height) * 2 - 1;
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 30, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 31, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Generate scattered small islands
float scatterNoise = OpenSimplex2S.Noise2( seed, nx * 6.0f, ny * 6.0f );
float baseNoise = OpenSimplex2S.Noise2( seed + 1, nx * 3.0f, ny * 3.0f ) * 0.5f;
float isletHeight = scatterNoise + baseNoise;
// Apply distance falloff to create isolated islets
float distance = MathF.Sqrt( nx * nx + ny * ny );
float falloff = Math.Clamp( 1 - distance * 1.5f, 0, 1 );
float heightValue = Math.Clamp( isletHeight * falloff, 0, 1 );
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Oceanic( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1;
float ny = (y / (float)height) * 2 - 1;
// Apply domain warping
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 40, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 41, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Generate large, continuous landmass with a few scattered features
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.0f, ny * 1.0f );
float featureNoise = OpenSimplex2S.Noise2( seed + 1, nx * 2.0f, ny * 2.0f ) * 0.5f;
float oceanicHeight = baseNoise + featureNoise;
// Apply radial falloff for a natural ocean/land mix
float distance = MathF.Sqrt( nx * nx + ny * ny );
float falloff = Math.Clamp( 1 - distance * 1.0f, 0, 1 );
float heightValue = Math.Clamp( oceanicHeight * falloff, 0, 1 );
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
}
Editor
library
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
namespace Sturnus.TerrainGenerationTool;
public static class Planetary
{
public static float Sharded(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool warp, // Apply domain warping
float warpSize, // Warp scale
float warpStrength // Warp strength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float shardHeight = 10f;
float crackDepth = 1f;
float noiseStrength = 0.05f;
int cellCount = 5;
// Apply domain warping for irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Generate cracks
float minDist = float.MaxValue;
float secondaryDist = float.MaxValue;
float cellSize = 2.0f / cellCount; // Normalize the cell size
for ( int i = 0; i < cellCount; i++ )
{
for ( int j = 0; j < cellCount; j++ )
{
float cellX = -1 + i * cellSize + OpenSimplex2S.Noise2( seed + 20, i, j ) * cellSize * 0.5f;
float cellY = -1 + j * cellSize + OpenSimplex2S.Noise2( seed + 21, i, j ) * cellSize * 0.5f;
float distance = MathF.Sqrt( (nx - cellX) * (nx - cellX) + (ny - cellY) * (ny - cellY) );
if ( distance < minDist )
{
secondaryDist = minDist;
minDist = distance;
}
else if ( distance < secondaryDist )
{
secondaryDist = distance;
}
}
}
// Compute shard height based on the secondary distance
float shardNoise = OpenSimplex2S.Noise2( seed + 30, nx, ny ) * noiseStrength;
float heightValue = MathF.Max( secondaryDist - minDist, 0f ) * shardHeight + shardNoise;
// Apply crack depth at borders between shards
if ( secondaryDist - minDist < 0.03f ) // Control the width of cracks
{
heightValue -= crackDepth;
}
// Add a baseline value to ensure no flat zero areas
//heightValue = MathF.Max( heightValue, minHeight );
// Clamp height to avoid negative values
heightValue = Math.Clamp( heightValue, 0, 1 );
float baseline = minHeight; // Minimum height
float heightValueCombined = MathF.Max( heightValue, baseline );
// Clamp the final height to valid range
return heightValueCombined;
}
public static float Craters(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool warp, // Apply domain warping for irregularity
float warpSize, // Warp scale
float warpStrength // Warp strength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
int craterCount = 100;
float minCraterSize = 0.1f;
float maxCraterSize = 0.3f;
float craterDepth = 0.05f;
float rimHeight = 0.05f;
float rimWidthRatio = 0.2f;
float noiseStrength = 0.05f;
float largeCraterRatio = 0.2f;
float slopeFalloff = 0.9f; // Controls smoothness of ramps
// Apply domain warping for irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Base terrain noise
float baseTerrain = OpenSimplex2S.Noise2( seed + 1, nx * 2.0f, ny * 2.0f ) * noiseStrength;
baseTerrain = (baseTerrain + 1) * 0.5f; // Normalize to [0, 1]
float heightValue = baseTerrain;
// Iterate through craters in reverse order to overwrite previous craters
for ( int i = craterCount - 1; i >= 0; i-- )
{
// Randomize crater properties
float craterX = random.Next( -100000, 100000 ) / 50000.0f;
float craterY = random.Next( -100000, 100000 ) / 50000.0f;
float craterRadius = (i < craterCount * largeCraterRatio)
? random.Next( (int)(maxCraterSize * 500), (int)(maxCraterSize * 1000) ) / 1000.0f // Large craters
: random.Next( (int)(minCraterSize * 500), (int)(minCraterSize * 1000) ) / 1000.0f; // Small craters
// Distance from the current point to the crater center
float distance = MathF.Sqrt( (nx - craterX) * (nx - craterX) + (ny - craterY) * (ny - craterY) );
if ( distance < craterRadius )
{
float rimStart = craterRadius * (1f - rimWidthRatio);
float rimEnd = craterRadius;
// Inside the pit
if ( distance < rimStart )
{
float pitFalloff = Math.Clamp( 1f - (distance / rimStart), 0f, 1f );
heightValue = baseTerrain - MathF.Pow( pitFalloff, slopeFalloff ) * craterDepth; // Smooth ramp to the center
}
// Raised rim
else if ( distance >= rimStart && distance < rimEnd )
{
float rimFalloff = Math.Clamp( (distance - rimStart) / (rimEnd - rimStart), 0f, 1f );
heightValue = baseTerrain + MathF.Pow( 1f - rimFalloff, slopeFalloff ) * rimHeight; // Rounded rim
}
// Reset terrain below the rim to prevent intersecting ridges
if ( distance >= rimEnd )
{
heightValue = baseTerrain;
}
// Exit the loop once the current crater is applied
break;
}
}
// Ensure height values are clamped to [0, 1]
heightValue = Math.Clamp( heightValue, 0, 1 );
return heightValue;
}
}
Editor
library
using System;
using System.Collections.Generic;
using Editor;
using Editor.TerrainEditor;
using Sandbox;
namespace Sturnus.TerrainGenerationTool.EditorTools;
/// <summary>
/// A terrain editor tool that includes every stock brush from the built-in terrain tool,
/// plus any custom brushes defined in this library. Register your own brushes by returning
/// them from <see cref="GetSubtools"/>.
/// </summary>
[EditorTool]
[Title( "Terrain Pro" )]
[Icon( "landscape" )]
[Alias( "tools.terrain-pro" )]
[Group( "Scene" )]
public class ExtendedTerrainTool : TerrainEditorTool
{
public override IEnumerable<EditorTool> GetSubtools()
{
// Stock brushes, minus the Hole tool (we don't want holes in Terrain Pro)
foreach ( var tool in base.GetSubtools() )
{
if ( tool is HoleTool )
continue;
yield return tool;
}
// Custom brushes from this library
yield return new BulgeBrushTool( this );
yield return new CraterBrushTool( this );
yield return new TerraceBrushTool( this );
yield return new NoiseBrushTool( this );
}
}
/// <summary>
/// Base for CPU-side sculpt brushes. The stock tools sculpt with a GPU compute shader;
/// these do the same on the CPU by directly editing the terrain's heightmap array, which
/// lets us invent entirely new brush behaviour without shipping a new shader.
/// Also keeps the painted region highlighted while the mouse is held down.
/// </summary>
public abstract class CpuSculptBrushTool : BaseBrushTool
{
ushort[] _strokeBefore;
RectInt _strokeRegion;
bool _strokeActive;
// Brush footprints stamped so far this stroke, for the circle highlight
List<(int cx, int cy, int size)> _strokeCircles;
// One terrain-projected decal per stamped circle, like the stock brush preview
List<BrushPreviewSceneObject> _highlightObjects;
// Deferred-apply selection: per-texel max falloff weight covered by the stroke
float[] _selectionWeights;
float _selectionOpacity;
/// <summary>
/// When true, painting only records the brush footprint (a selection) instead of sculpting.
/// The whole selection is sculpted at once in <see cref="ApplySelection"/> when the mouse is
/// released, so the effect lands across the entire dragged area simultaneously.
/// </summary>
protected virtual bool ApplyOnRelease => false;
public override bool PaintMode { get; set; } = true;
protected CpuSculptBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )
{
}
public override void OnUpdate()
{
base.OnUpdate();
// Keep the brush circles we've painted highlighted until the mouse is released
if ( _strokeActive )
{
var terrain = GetSelectedComponent<Terrain>() ?? Scene.Get<Terrain>();
if ( terrain.IsValid() )
UpdateStrokeHighlight( terrain );
}
}
/// <summary>
/// Renders a translucent brush-preview decal for every circle stamped during the stroke.
/// Uses the same terrain-projected <see cref="BrushPreviewSceneObject"/> the stock terrain
/// tool shows for its brush preview, so the highlight hugs the terrain surface.
/// </summary>
void UpdateStrokeHighlight( Terrain terrain )
{
int res = terrain.Storage.Resolution;
if ( res <= 0 || _strokeCircles == null ) return;
// Grow/shrink the decal list to match the number of stamped circles
if ( _highlightObjects == null )
_highlightObjects = new List<BrushPreviewSceneObject>();
while ( _highlightObjects.Count < _strokeCircles.Count )
_highlightObjects.Add( new BrushPreviewSceneObject( Gizmo.World ) );
while ( _highlightObjects.Count > _strokeCircles.Count )
{
var extra = _highlightObjects[^1];
extra.Delete();
_highlightObjects.RemoveAt( _highlightObjects.Count - 1 );
}
var tx = terrain.WorldTransform;
float heightScale = terrain.Storage.TerrainHeight / 65535f;
float unitsPerTexel = terrain.Storage.TerrainSize / (float)res;
for ( int i = 0; i < _strokeCircles.Count; i++ )
{
var (cx, cy, size) = _strokeCircles[i];
int hx = Math.Clamp( cx, 0, res - 1 );
int hy = Math.Clamp( cy, 0, res - 1 );
float h = terrain.Storage.HeightMap[hy * res + hx] * heightScale;
var obj = _highlightObjects[i];
obj.RenderLayer = SceneRenderLayer.OverlayWithDepth;
obj.Bounds = BBox.FromPositionAndSize( 0, float.MaxValue );
obj.Transform = new Transform( tx.PointToWorld( new Vector3( cx * unitsPerTexel, cy * unitsPerTexel, h ) ), tx.Rotation );
obj.Radius = size * 0.5f * unitsPerTexel;
obj.Texture = TerrainEditorTool.Brush?.Texture;
obj.Color = Color.FromBytes( 255, 165, 0 ).WithAlpha( 0.5f );
}
}
/// <summary>
/// Deletes the highlight decals, called when the stroke ends.
/// </summary>
void ClearStrokeHighlight()
{
if ( _highlightObjects != null )
{
foreach ( var obj in _highlightObjects )
obj.Delete();
_highlightObjects.Clear();
}
}
protected override void OnPaint( Terrain terrain, TerrainPaintParameters paint )
{
int res = terrain.Storage.Resolution;
// Brush footprint in texels
int size = (int)Math.Floor( paint.BrushSettings.Size * 2.0f / terrain.Storage.TerrainSize * res );
size = Math.Max( size, 1 );
int cx = (int)Math.Floor( paint.HitUV.x * res );
int cy = (int)Math.Floor( paint.HitUV.y * res );
var region = new RectInt( cx - size / 2, cy - size / 2, size + 1, size + 1 );
// On the first paint of a stroke, snapshot the entire heightmap so undo can restore it
// no matter how far the stroke drags.
if ( !_strokeActive )
{
_strokeActive = true;
_strokeRegion = region;
_strokeBefore = (ushort[])terrain.Storage.HeightMap.Clone();
_strokeCircles = new List<(int, int, int)>();
if ( ApplyOnRelease )
{
_selectionWeights = new float[res * res];
_selectionOpacity = paint.BrushSettings.Opacity;
}
}
else
{
// Expand the dirty region to cover this frame's footprint
int left = Math.Min( _strokeRegion.Left, region.Left );
int top = Math.Min( _strokeRegion.Top, region.Top );
int right = Math.Max( _strokeRegion.Right, region.Right );
int bottom = Math.Max( _strokeRegion.Bottom, region.Bottom );
_strokeRegion = new RectInt( left, top, right - left, bottom - top );
}
_strokeCircles.Add( (cx, cy, size) );
if ( ApplyOnRelease )
{
// Only mark the selection - no height changes until the mouse is released
StampSelection( terrain, paint, res, cx, cy, size );
}
else
{
// Let the brush decide how to edit each texel
Sculpt( terrain, paint, res, cx, cy, size );
// Upload CPU -> GPU and refresh collision so the sculpt shows live
terrain.SyncGPUTexture();
terrain.UpdateCollision( Terrain.SyncFlags.Height, _strokeRegion );
}
}
protected abstract void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size );
/// <summary>
/// Records the brush's falloff weight for every texel in the footprint. The strongest weight
/// any stamp leaves on a texel wins, so overlapping circles blend into one clean selection.
/// </summary>
void StampSelection( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )
{
int radius = size / 2;
for ( int y = -radius; y <= radius; y++ )
{
for ( int x = -radius; x <= radius; x++ )
{
int tx = centerX + x;
int ty = centerY + y;
if ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;
float w = SampleBrush( paint, x + radius, y + radius, size );
if ( w <= 0.001f ) continue;
int index = ty * res + tx;
if ( w > _selectionWeights[index] )
_selectionWeights[index] = w;
}
}
}
/// <summary>
/// Applies the brush over the whole recorded selection at once. Called on mouse release when
/// <see cref="ApplyOnRelease"/> is true. The weights array holds the max falloff weight for
/// every texel touched by the stroke.
/// </summary>
protected virtual void ApplySelection( Terrain terrain, int res, float[] weights, float opacity )
{
}
protected override void OnPaintEnded( Terrain terrain )
{
if ( _strokeActive )
{
int res = terrain.Storage.Resolution;
// Clamp the unioned dirty region to the terrain bounds
_strokeRegion.Left = Math.Clamp( _strokeRegion.Left, 0, res - 1 );
_strokeRegion.Right = Math.Clamp( _strokeRegion.Right, 0, res - 1 );
_strokeRegion.Top = Math.Clamp( _strokeRegion.Top, 0, res - 1 );
_strokeRegion.Bottom = Math.Clamp( _strokeRegion.Bottom, 0, res - 1 );
if ( ApplyOnRelease && _selectionWeights != null )
{
// Sculpt the entire dragged selection in one go, then sync once
ApplySelection( terrain, res, _selectionWeights, _selectionOpacity );
terrain.SyncGPUTexture();
terrain.UpdateCollision( Terrain.SyncFlags.Height, _strokeRegion );
}
ushort[] after = (ushort[])terrain.Storage.HeightMap.Clone();
var region = _strokeRegion;
Action Restore( ushort[] data ) => () =>
{
if ( !terrain.IsValid() ) return;
WriteHeightRegion( terrain.Storage.HeightMap, res, region, data );
terrain.SyncGPUTexture();
terrain.UpdateCollision( Terrain.SyncFlags.Height, region );
};
SceneEditorSession.Active.UndoSystem.Insert( $"Terrain {DisplayInfo.For( this ).Name}", Restore( _strokeBefore ), Restore( after ) );
_strokeBefore = null;
_strokeActive = false;
_strokeCircles = null;
_selectionWeights = null;
ClearStrokeHighlight();
}
}
/// <summary>
/// Sample the selected brush's falloff at a local footprint coordinate.
/// Returns 0..1, 1 in the middle, 0 at the edges.
/// </summary>
protected float SampleBrush( TerrainPaintParameters paint, int localX, int localY, int size )
{
if ( size <= 0 ) return 0f;
var pixmap = paint.Brush?.Pixmap;
if ( pixmap != null && pixmap.Width > 0 && pixmap.Height > 0 )
{
float u = (localX + 0.5f) / size;
float v = (localY + 0.5f) / size;
int px = Math.Clamp( (int)(u * pixmap.Width), 0, pixmap.Width - 1 );
int py = Math.Clamp( (int)(v * pixmap.Height), 0, pixmap.Height - 1 );
var c = pixmap.GetPixel( px, py );
return Math.Clamp( c.r, 0f, 1f );
}
// Fallback: soft radial falloff
float nx = (localX + 0.5f - size * 0.5f) / (size * 0.5f);
float ny = (localY + 0.5f - size * 0.5f) / (size * 0.5f);
float d = MathF.Sqrt( nx * nx + ny * ny );
return Math.Clamp( 1f - d, 0f, 1f );
}
/// <summary>
/// Writes a full heightmap snapshot into the given dirty region. The snapshot may be a full
/// map clone, but we only copy the region we actually painted so collision updates stay cheap.
/// </summary>
static void WriteHeightRegion( ushort[] heightmap, int res, RectInt region, ushort[] data )
{
for ( int y = 0; y < region.Height; y++ )
{
for ( int x = 0; x < region.Width; x++ )
{
heightmap[region.Left + x + (region.Top + y) * res] = data[region.Left + x + (region.Top + y) * res];
}
}
}
}
/// <summary>
/// Raises a smooth dome inside the brush footprint.
/// </summary>
[Title( "Bulge" )]
[Icon( "bubble_chart" )]
[Alias( "tools.terrain.bulge" )]
[Group( "1" )]
[Order( 1 )]
public class BulgeBrushTool : CpuSculptBrushTool
{
public BulgeBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )
{
}
protected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )
{
var heightmap = terrain.Storage.HeightMap;
float opacity = paint.BrushSettings.Opacity;
int radius = size / 2;
for ( int y = -radius; y <= radius; y++ )
{
for ( int x = -radius; x <= radius; x++ )
{
int tx = centerX + x;
int ty = centerY + y;
if ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;
float brush = SampleBrush( paint, x + radius, y + radius, size );
if ( brush <= 0.001f ) continue;
int index = ty * res + tx;
float current = heightmap[index] / 65535f;
// Parabolic dome: 1 at centre, 0 at the edge
float dome = brush * brush;
float target = current + dome * opacity;
heightmap[index] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );
}
}
}
}
/// <summary>
/// Digs a rounded depression with a slightly raised rim, like an impact crater.
/// </summary>
[Title( "Crater" )]
[Icon( "brightness_low" )]
[Alias( "tools.terrain.crater" )]
[Group( "1" )]
[Order( 1 )]
public class CraterBrushTool : CpuSculptBrushTool
{
public CraterBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )
{
}
protected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )
{
var heightmap = terrain.Storage.HeightMap;
float opacity = paint.BrushSettings.Opacity;
int radius = size / 2;
for ( int y = -radius; y <= radius; y++ )
{
for ( int x = -radius; x <= radius; x++ )
{
int tx = centerX + x;
int ty = centerY + y;
if ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;
float brush = SampleBrush( paint, x + radius, y + radius, size );
if ( brush <= 0.001f ) continue;
int index = ty * res + tx;
float current = heightmap[index] / 65535f;
// Depression with a raised rim: dip in the middle, bump near the edge
float rim = brush < 0.75f ? -brush : (brush - 0.75f) / 0.25f;
float target = current + rim * opacity;
heightmap[index] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );
}
}
}
}
/// <summary>
/// Snaps heights to evenly spaced terraced steps within the brush footprint.
/// </summary>
[Title( "Terrace" )]
[Icon( "stairs" )]
[Alias( "tools.terrain.terrace" )]
[Group( "1" )]
[Order( 1 )]
public class TerraceBrushTool : CpuSculptBrushTool
{
public TerraceBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )
{
}
protected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )
{
var heightmap = terrain.Storage.HeightMap;
float opacity = paint.BrushSettings.Opacity;
int radius = size / 2;
// Step every 8% of full height - you could expose this as a setting later
const float stepSize = 0.08f;
for ( int y = -radius; y <= radius; y++ )
{
for ( int x = -radius; x <= radius; x++ )
{
int tx = centerX + x;
int ty = centerY + y;
if ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;
float brush = SampleBrush( paint, x + radius, y + radius, size );
if ( brush <= 0.001f ) continue;
int index = ty * res + tx;
float current = heightmap[index] / 65535f;
float stepped = MathF.Round( current / stepSize ) * stepSize;
float target = MathX.LerpTo( current, stepped, opacity * brush );
heightmap[index] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );
}
}
}
}
/// <summary>
/// Adds adjustable simplex noise to the terrain, driven by the library's OpenSimplex2S
/// generator. Frequency, strength and seed can be tuned with the toolbar sliders.
/// </summary>
[Title( "Noise" )]
[Icon( "shuffle" )]
[Alias( "tools.terrain.noise" )]
[Group( "1" )]
[Order( 1 )]
public class NoiseBrushTool : CpuSculptBrushTool
{
/// <summary>Noise frequency - higher = more, smaller bumps.</summary>
[Property, Range( 0.5f, 20f ), Step( 0.1f ), WideMode] public float Frequency { get; set; } = 4f;
/// <summary>How strongly the noise displaces the height (0..1, fraction of full height).</summary>
[Property, Range( 0.001f, 0.05f ), Step( 0.001f ), WideMode] public float Strength { get; set; } = 0.008f;
/// <summary>Random seed for the noise field.</summary>
[Property, Range( 0, 100000 ), Step( 1 ), WideMode] public int NoiseSeed { get; set; } = 1337;
public NoiseBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )
{
}
// Noise is applied once across the whole dragged selection when the mouse is released,
// not per-frame while painting.
protected override bool ApplyOnRelease => true;
/// <summary>
/// Shows the stock terrain brush settings plus a "Noise Settings" group bound to the
/// noise properties, so Frequency / Strength / Seed can be tuned in the sidebar.
/// </summary>
public override Widget CreateToolSidebar()
{
if ( _parent is null ) return null;
var sidebar = (ToolSidebarWidget)_parent.CreateToolSidebar();
if ( sidebar is null ) return null;
var so = EditorTypeLibrary.GetSerializedObject( this );
var group = sidebar.AddGroup( "Noise Settings" );
var sheet = new ControlSheet();
sheet.AddObject( so, prop =>
prop.Name is nameof( Frequency ) or nameof( Strength ) or nameof( NoiseSeed ) );
group.Add( sheet );
return sidebar;
}
// Not used - ApplyOnRelease routes painting through ApplySelection instead.
protected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )
{
}
/// <summary>
/// Applies the noise field across every texel covered by the stroke, using the strongest
/// brush falloff weight recorded for each texel.
/// </summary>
protected override void ApplySelection( Terrain terrain, int res, float[] weights, float opacity )
{
var heightmap = terrain.Storage.HeightMap;
for ( int i = 0; i < weights.Length; i++ )
{
float w = weights[i];
if ( w <= 0.001f ) continue;
int tx = i % res;
int ty = i / res;
// Sample simplex noise at the texel, in [0,1], then remap to [-1,1]
// so the noise can both add and subtract height.
float noise = OpenSimplex2S.Noise2( NoiseSeed, tx * Frequency, ty * Frequency );
noise = noise * 2f - 1f;
float current = heightmap[i] / 65535f;
float target = current + noise * Strength * opacity * w;
heightmap[i] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );
}
}
public override Widget CreateToolbarWidget()
{
var group = new Widget();
group.FixedHeight = Theme.RowHeight;
group.Layout = Layout.Row();
group.Layout.Spacing = 6;
group.Layout.Add( new Label( "Frequency" ) );
var freq = new FloatSlider( group );
freq.Minimum = 0.5f;
freq.Maximum = 20f;
freq.Step = 0.1f;
freq.Value = Frequency;
freq.OnValueEdited = () => Frequency = freq.Value;
group.Layout.Add( freq, 1 );
group.Layout.Add( new Label( "Strength" ) );
var strength = new FloatSlider( group );
strength.Minimum = 0.001f;
strength.Maximum = 0.05f;
strength.Step = 0.001f;
strength.Value = Strength;
strength.OnValueEdited = () => Strength = strength.Value;
group.Layout.Add( strength, 1 );
group.Layout.Add( new Label( "Seed" ) );
var seed = new FloatSlider( group );
seed.Minimum = 0;
seed.Maximum = 100000;
seed.Step = 1;
seed.Value = NoiseSeed;
seed.OnValueEdited = () => NoiseSeed = (int)seed.Value;
group.Layout.Add( seed, 1 );
group.OnPaintOverride = () =>
{
Paint.ClearPen();
Paint.SetBrush( Theme.ControlBackground );
Paint.DrawRect( group.LocalRect, Theme.ControlRadius );
return true;
};
return group;
}
}
Game
library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Terrain Pro" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "terraingenerationtool" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "sturnus" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "sturnus.terraingenerationtool" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-08-06T23:17:13.0077717Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.327.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.327.0")]
Game
library
using Sandbox;
Game
library
using Sandbox;
Editor
library
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
namespace Sturnus.TerrainGenerationTool.Noise.Lanczos
{
//A version of value-noise using Lanczos-Resampling. Also has classic billiniar-noise
//Made by Zomare
class ValueNoise
{
//Hash-Function for rng, outputs in range [-1, 1]
static float Random( int x1, int y1 )
{
byte[] table = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7,
225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247,
120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134,
139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220,
105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80,
73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38,
147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189,
28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101,
155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232,
178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12,
191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181,
199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236,
205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180};
return ((float)table[(x1 + table[y1 & 255]) & 255] / 255f) * 2 - 1;
}
public static float Compute( float x, float y )
{
int ix = (int)x;
int iy = (int)y;
float dx = x - ix;
float dy = y - iy;
//averages used for normalizing
float avgY = 0;
float avgX = 0;
//Calculating lookup table for faster horizontal interpolation
float[] lanczosX = new float[6];
for ( int px = -2; px < 4; px++ )
{
float f = Lanczos( dx - px );
avgX += f;
lanczosX[px + 2] = f;
}
float n = 0;
for ( int py = -2; py < 4; py++ )
{
float a = 0;
for ( int px = -2; px < 4; px++ )
{
a += Random( ix + px, iy + py ) * lanczosX[px + 2];
}
a /= avgX;
n += a * Lanczos( dy - py );
avgY += Lanczos( dy - py );
}
//!Not correctly normalized!
return smoothstep( -1, 1, (n / avgY / 1.25f + 1) / 2f );
}
//Lanczos function used for interpolation
//L(x)=sinc(x)sinc(x/a)
static float Lanczos( float t )
{
if ( t == 0 )
{
return 1;
}
else if ( t > 4 || t < -4 )
{
return 0;
}
return 3 * (float)((Math.Sin( Math.PI * t ) * Math.Sin( Math.PI * (t / 3) )) / (Math.PI * Math.PI * t * t));
}
//Left in for the purpose of maybe using it later
static float Sinc( float x )
{
return (float)(Math.Sin( Math.PI * x ) / (Math.PI * x));
}
//Outputs an low frequency octave of noise as a png
/*static public void Test( int d )
{
var bm = new Bitmap( d, d );
int off = new Random().Next( -1000, 1000 );
for ( int y = 0; y < d; y++ )
{
for ( int x = 0; x < d; x++ )
{
int c = (int)(255 * (Compute( x * 0.025f, y * 0.025f + off ) + 1) / 2f);
bm.SetPixel( x, y, Color.FromArgb( c, c, c ) );
}
}
bm.Save( "test.png" );
}*/
//Generates a worldmap and outputs it as a png
/*static public void Generate( int d )
{
var bm = new Bitmap( d, d );
int off = new Random().Next( -1000, 1000 );
for ( int y = 0; y < d; y++ )
{
for ( int x = 0; x < d; x++ )
{
float a = Math.Abs( ComputeFractal( x * 0.02f, y * 0.02f + off, 4 ) );
if ( a > 0.2f )
{
a = 0.2f;
}
a /= 0.2f;
a *= ComputeFractal( x * 0.025f, y * 0.025f + 1000 + off, 8 ) * 0.5f + 0.5f;
Color c = Color.Aqua;
if ( a > 0.9f )
{
c = Color.White;
}
else if ( a > 0.75f )
{
c = Color.LightGray;
}
else if ( a > 0.4f )
{
c = Color.ForestGreen;
}
bm.SetPixel( x, y, c );
}
}
bm.Save( "map.png" );
}*/
//Fractal Noise; n: amount of octaves
static public float ComputeFractal( float x, float y, int n )
{
float a = 0;
float avg = 0;
float w = 1;
float frq = 1;
for ( int i = 0; i < n; i++ )
{
a += Compute( x * frq, y * frq ) * w;
avg += w;
w *= 0.25f;
frq *= 3;
}
return a / avg;
}
//"Normal" Value-Noise
static public float ComputeLinear( float x, float y )
{
int ix = (int)x;
int iy = (int)y;
float dx = x - ix;
float dy = y - iy;
float fx1 = lerp( Random( ix, iy ), Random( ix + 1, iy ), dx );
float fx2 = lerp( Random( ix, iy + 1 ), Random( ix + 1, iy + 1 ), dx );
return lerp( fx1, fx2, dy );
}
//Basic interpolation Functions
//7th order smoothstep
static float smootherstep( float a1, float a2, float t )
{
return lerp( a1, a2, t * t * t * t * (t * (t * (70 - 20 * t) - 84) + 35) );
}
//Normal smoothstep
static float smoothstep( float a1, float a2, float t )
{
return lerp( a1, a2, t * t * (3 - 2 * t) );
}
static float lerp( float a1, float a2, float t )
{
t = Math.Clamp( t, 0f, 1f );
return (1 - t) * a1 + t * a2;
}
}
}
Editor
library
using Editor;
using Sandbox;
using Sandbox.UI;
using System;
namespace Sturnus.TerrainGenerationTool;
public static class Volcanic
{
public static float Default(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool warp,
float warpSize,
float warpStrength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float craterRadius = 0.3f; // Average radius of the central crater
float rimHeight = 0.25f; // Height of the crater rim
float rimWidth = 0f; // Width of the rim
float outerSlopeStrength = 0.5f; // Strength of the gradient for the exterior slope
float innerSlopeStrength = 2.0f; // Strength of the gradient for the interior slope
float baseHeight = 0.1f; // Minimum base height
float noiseStrength = 0.02f; // General noise strength
float distance = MathF.Sqrt( nx * nx + ny * ny );
// Calculate distance from the center of the heightmap
float centerX = 0f, centerY = 0f; // Center of the heightmap
float distanceToCenter = MathF.Sqrt( (nx - centerX) * (nx - centerX) + (ny - centerY) * (ny - centerY) );
// Apply domain warping for irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Introduce irregularity to the crater radius
float craterIrregularity = OpenSimplex2S.Noise2( seed + 20, nx * 4.0f, ny * 4.0f ) * 0.1f;
float dynamicCraterRadius = craterRadius + craterIrregularity;
// Initialize height components
float crater = 0f;
float rim = 0f;
float outerSlope = 0f;
// Crater: Smooth inward slope towards the center of the crater
if ( distanceToCenter < dynamicCraterRadius )
{
float craterDepth = (1f - (distanceToCenter));
crater = MathF.Pow( craterDepth, innerSlopeStrength ) ; // Inward slope
}
// Rim: Uneven ridge around the crater
if ( distanceToCenter >= dynamicCraterRadius && distanceToCenter < dynamicCraterRadius + rimWidth )
{
float rimFalloff = (distanceToCenter - dynamicCraterRadius) / rimWidth;
float rimNoise = OpenSimplex2S.Noise2( seed + 30, nx * 8.0f, ny * 8.0f ) * noiseStrength;
rim = (1f - rimFalloff) * rimHeight + rimNoise; // Add noise for unevenness
}
// Outer slope: Smooth gradient with noise towards the edges
if ( distanceToCenter >= dynamicCraterRadius + rimWidth )
{
float slopeDistance = 1f - distanceToCenter; // Decrease height as we approach the edge
float slopeNoise = OpenSimplex2S.Noise2( seed + 40, nx * 4.0f, ny * 4.0f ) * noiseStrength;
outerSlope = MathF.Max( 0, slopeDistance ) * outerSlopeStrength + slopeNoise;
}
// Combine components
float heightValue = baseHeight + crater + rim + outerSlope;
// Smooth transition towards the crater center for a more natural look
if ( distanceToCenter < dynamicCraterRadius )
{
float centerFalloff = MathF.Pow( 1f - (distanceToCenter / dynamicCraterRadius), 2f );
heightValue = centerFalloff * baseHeight; // Slight bump for a smoother slope
}
heightValue = Math.Max( heightValue, baseHeight );
var heightValueBase = Math.Max( heightValue, minHeight );
// Clamp the final height
return Math.Clamp( heightValueBase, 0, 1 );
}
}
Editor
library
using Editor;
using Sandbox;
using System;
namespace Sturnus.TerrainGenerationTool;
public static class Mountainous
{
public static float Default( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize = 0.1f, float warpStrength = 0.5f )
{
float nx = x / (float)width; // Normalize x to range [0, 1]
float ny = y / (float)height; // Normalize y to range [0, 1]
float warpX;
float warpY;
float warpedNx;
float warpedNy;
if( warp )
{
// Generate warp offsets using noise
warpX = OpenSimplex2S.Noise2( seed + 20, nx * warpSize, ny * warpSize ) * warpStrength;
warpY = OpenSimplex2S.Noise2( seed + 21, nx * warpSize, ny * warpSize ) * warpStrength;
// Apply domain warping to the coordinates
warpedNx = nx + warpX;
warpedNy = ny + warpY;
}
else
{
warpedNx = nx;
warpedNy = ny;
}
// Base noise for ridge structure (using warped coordinates)
float ridgeNoise = Math.Abs( OpenSimplex2S.Noise2( seed, warpedNx * 5, warpedNy * 0.5f ) ) * 0.8f;
// Add distortion to the ridge line to make it less uniform
float distortion = OpenSimplex2S.Noise2( seed + 2, warpedNx * 2, warpedNy * 2 ) * 0.3f;
ridgeNoise += distortion;
// Add fine detail to the mountains with higher frequency noise
float detailNoise = OpenSimplex2S.Noise2( seed + 1, warpedNx * 20, warpedNy * 20 ) * 0.2f;
// Combine ridge, distortion, and detail noise
float combinedNoise = ridgeNoise + detailNoise;
// Apply a falloff effect to keep the edges lower
float edgeFalloff = 1.0f - Math.Clamp( Math.Abs( nx - 0.5f ) + Math.Abs( ny - 0.5f ), 0, 1 );
// Combine all effects
float heightValue = combinedNoise * edgeFalloff;
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
heightValue = MathF.Max( heightValue, baseline );
// Combine everything with edge falloff
return Math.Clamp( heightValue, 0, 1 );
}
}
Editor
library
using Editor;
using Sandbox;
using System;
namespace Sturnus.TerrainGenerationTool;
public static class Sea
{
public static float SeaBed(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool domainWarping,
float domainWarpingSize,
float domainWarpingStrength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float depthScale = 0.4f; // Adjust the overall depth
float waveFrequency = 0.5f; // Frequency of base ripples
float waveAmplitude = 0.1f; // Height of ripples
float randomVariation = 0.02f; // Subtle randomness
float distortionFrequency = 0.03f; // Frequency for distortion
float distortionStrength = 0.05f ; // Strength of distortion
// Normalize coordinates to [0, 1]
float nx = x / (float)width;
float ny = y / (float)height;
// Generate base ripple effect
float baseRipple = MathF.Sin( nx * waveFrequency * MathF.PI * 2 ) * waveAmplitude
+ MathF.Sin( ny * waveFrequency * MathF.PI * 2 ) * waveAmplitude;
// Add distortion to break uniformity
float distortion = OpenSimplex2S.Noise2( seed + 1, nx * distortionFrequency, ny * distortionFrequency )
* distortionStrength;
// Add random noise for natural variation
float randomNoise = (float)(random.NextDouble() - 0.5) * randomVariation;
// Combine all effects
float heightValue = baseRipple + distortion + randomNoise;
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
heightValue = MathF.Max( heightValue, baseline );
// Scale to depth and clamp
heightValue = heightValue * depthScale;
return Math.Clamp( heightValue, 0.0f, 1.0f );
}
public static float Cliff(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool warp,
float warpSize,
float warpStrength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float hillHeight = 0.9f; // Height of the cliff
float slopeWidth = 0.2f; // Width of the slope transition
float wideningFactor = 0.9f;
// Apply domain warping for cliff irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Calculate distance for the hill gradient
float distance = nx >= 0 ? MathF.Abs( nx ) : MathF.Abs( nx ) * (1 - wideningFactor); // Widen on one side
// Generate hill gradient using a smooth transition
float hill = Math.Clamp( 1.0f - MathF.Pow( distance / slopeWidth, 2.0f ), 0, 1 ); // Quadratic falloff for smoother slope
hill *= hillHeight; // Scale the hill to the desired height
// Add base noise for texture
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 6.0f, ny * 6.0f ) * 0.2f;
// Add finer noise for additional detail
float fineNoise = OpenSimplex2S.Noise2( seed + 1, nx * 12.0f, ny * 12.0f ) * (0.2f / 2);
// Combine hill gradient with noise
float heightValue = hill + baseNoise + fineNoise;
float baseValue = Math.Max( baseNoise, minHeight );
heightValue = Math.Max( heightValue, baseValue );
// Clamp the height value to ensure valid results
return Math.Clamp( heightValue, 0, 1 );
}
}
Editor
library
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Drawing;
using System.IO;
using System.Linq;
using Editor;
using Editor.ShaderGraph.Nodes;
using Editor.Widgets;
using Sandbox;
using SkiaSharp;
using static Sandbox.Gradient;
using Sturnus.TerrainGenerationTool;
using Sandbox.Utility;
using System.Threading;
using System.Threading.Tasks;
using Parallel = System.Threading.Tasks.Parallel;
using Sturnus.TerrainGenerationTool.RiverStream;
using Sandbox.Services;
using System.Reflection;
using static TerrainGenerationTool;
[EditorApp( "Terrain Pro Generation", "terrain", "Generate procedural terrain with a realtime 3D preview" )]
public class TerrainGenerationTool : BaseWindow
{
public string GenerationPath { get; set; } = Editor.FileSystem.Content.GetFullPath( "" ) + "\\TerrainGenerationTool\\";
public string GenerationLocalPath { get; set; } = "\\TerrainGenerationTool\\";
public string ExportPath { get; set; } = Project.Current.RootDirectory + "\\Assets\\";
HashSet<string> TerrainCategoryArray { get; set; } = new HashSet<string>();
HashSet<string> TerrainShapeArray { get; set; } = new HashSet<string>();
// Per-tile category/shape selections for the tile grid (index = ty * grid + tx)
string[] _tileCategories = new string[1];
string[] _tileShapes = new string[1];
// Per-tile height/scale/seed values (index = ty * grid + tx)
float[] _tileMinHeights = new float[1];
float[] _tileMaxHeights = new float[1];
float[] _tilePlaneScales = new float[1];
long[] _tileSeeds = new long[1];
// Per-tile smoothing/noise values (index = ty * grid + tx)
int[] _tileSmoothingPasses = new int[1];
int[] _tileNoiseLayerStacks = new int[1];
// Per-tile domain warping values (index = ty * grid + tx)
bool[] _tileDomainWarping = new bool[1];
float[] _tileDomainWarpingSizes = new float[1];
float[] _tileDomainWarpingStrengths = new float[1];
// Per-tile splatmap settings (index = ty * grid + tx)
int[] _tileSplatLayerCounts = new int[1];
int[] _tileSplatMapCounts = new int[1];
SplatDispersionMode[] _tileSplatDispersions = new SplatDispersionMode[1];
float[] _tileSplatBlendStrengths = new float[1];
// The tile currently being edited by the Terrain Type page's Category/Shape selectors
int _selectedTileIndex = 0;
bool _syncingTileSelectors;
List<TileGridBox> _tileBoxes = new();
List<Type> terrainCategoryClassesTypes = new List<Type> { typeof( Islands ), typeof( Mountainous ), typeof( Planetary ), typeof( Realistic ), typeof( Sea ), typeof( Volcanic ) };
List<Type> terrainShapeMethodTypes { get; set; }
enum TerrainDimensions : int
{
x512 = 512,
x1024 = 1024,
x2048 = 2048,
x4096 = 4096,
X8192 = 8192
}
/// <summary>
/// How the grid is stored once generated.
/// Combined stitches every cell into one full-res map; PerCell keeps each cell as its own
/// full-resolution heightmap/splatmap (for per-terrain apply, per-cell export and preview).
/// </summary>
public enum GridStorageMode
{
Combined,
PerCell
}
[Step( 1 )] GridStorageMode GridStorage { get; set; } = GridStorageMode.Combined;
// Per-cell full-resolution maps (index = ty * grid + tx). Only filled in PerCell mode.
List<float[,]> _cellHeightmaps = new();
List<float[,]> _cellSplatmaps = new();
public enum SplatDispersionMode
{
Evenly,
Natural
}
//enum TerrainCategoryEnum;
DynamicEnum TerrainCategoryEnum = new DynamicEnum();
DynamicEnum TerrainShapeEnum = new DynamicEnum();
TerrainDimensions TerrainDimensionsEnum { get; set; } = TerrainDimensions.x512;
[Step( 1 ), MinMax( 1, 4 )] int TerrainGridSize { get; set; } = 1;
//TerrainCategoryEnum TerrainShapeEnumSelect { get; set; }
[Step( 0.01f ),MinMax(0.1f,1f)] float TerrainMinHeight { get; set; } = 0.2f;
[Step( 0.01f),MinMax(0.1f,1f)] float TerrainMaxHeight { get; set; } = 0.5f;
[Step( 0.01f),MinMax(0.1f,1f)] float TerrainPlaneScale { get; set; } = 0.5f;
long TerrainSeed { get; set; } = 1234567890;
[Step( 1 ), MinMax( 1,20 )] int SmoothingPasses { get; set; } = 10;
[Group( "Domain Warping" )] bool DomainWarping { get; set; } = true;
[Group( "Domain Warping" )][Step( 0.01f ),MinMax(0.1f,1f)] float DomainWarpingSize { get; set; } = 0.25f;
[Group( "Domain Warping" )][Step( 0.01f ),MinMax(0.1f,1f)] float DomainWarpingStrength { get; set; } = 0.15f;
bool ErosionSimulation { get; set; } = false;
[Step( 1f),MinMax(1f,25f)] int NoiseLayerStacks { get; set; } = 1;
///
/// River Carving Variables
///
[Property][Group( "River & Stream Carving" )] bool RiverCarvingBool { get; set; } = true;
[Property][Group( "River & Stream Carving" )][Step( 0.1f), MinMax( 0.5f, 10f )] float RiverCarvingFrequency { get; set; } = 1.5f;
[Property][Group( "River & Stream Carving" )][Step( 0.01f),MinMax(0.01f,5f)] float RiverCarvingStrength { get; set; } = 0.3f;
[Property][Group( "River & Stream Carving" )][Step( 0.001f),MinMax(0.01f,0.25f)] float RiverCarvingDepth { get; set; } = 0.01f;
[Property][Group( "River & Stream Carving" )][Step( 0.01f),MinMax(0.001f,2f)] float RiverCarvingWidth { get; set; } = 0.25f;
[Property][Group( "River & Stream Carving" )][Step( 0.01f),MinMax(0.05f,1f)] float RiverCarvingSpacing { get; set; } = 0.05f;
[Property][Group( "River & Stream Carving" )][Step( 0.01f),MinMax(0.01f,1f)] float RiverCarvingTurbulenceStrength { get; set; } = 0.01f;
[Property][Group( "River & Stream Carving" )][Step( 0.01f),MinMax(0.01f,10f)] float RiverCarvingTurbulenceFrequency { get; set; } = 0.01f;
///
/// Tool Placement Square
///
[Group( "Tool Placement" )] bool StagingArea { get; set; } = true;
[Group( "Tool Placement" )][Step( 1),MinMax( 1, 100 )] int StagingAreaSize { get; set; } = 10; // Size of the square (in grid units)
[Group( "Tool Placement" )][Step(0.01f),MinMax(0,1)] float StagingAreaHeight { get; set; } = 0.1f; // Height of the flat square
[Group( "Tool Placement" )][Step(0.01f),MinMax(0,1)] float StagingAreaX { get; set; } = 0.1f; // X-center of the square as a ratio
[Group( "Tool Placement" )][Step(0.01f),MinMax(0,1)] float StagingAreaY { get; set; } = 0.1f; // Y-center of the square as a ratio
Gradient SplatMapGradient = new Gradient( new Gradient.ColorFrame( 0.0f, Color.Cyan ), new Gradient.ColorFrame( 0.25f, Color.Red ), new Gradient.ColorFrame( 0.5f, Color.Yellow ), new Gradient.ColorFrame( 0.75f, Color.Green ) );
SKColor[] _splatcolors { get; set; }
[Step( 1 ), MinMax( 2, 32 )] int SplatLayerCount { get; set; } = 8;
[Step( 1 ), MinMax( 1, 8 )] int SplatMapCount { get; set; } = 1;
SplatDispersionMode SplatDispersion { get; set; } = SplatDispersionMode.Evenly;
[Step( 0.05f ), MinMax( 0f, 1f )] float SplatBlendStrength { get; set; } = 0.35f;
[Property] bool PreviewSplatMaterials { get; set; } = false;
float[] _splatthresholds = { 0f, 0.25f, 0.50f, 0.75f };
float[,] _heightmap;
float[,] _splatmap;
float[,] _previewHeightmap;
TerrainMaterial[] _previewMaterials;
int _previewMaterialsGeneration = 0;
List<Editor.Asset> _localTmatAssets;
Texture _preview_image_texture;
Editor.TextureWidget PreviewImage;
Texture _preview_splatmap_texture;
Editor.TextureWidget PreviewSplatmap;
SceneRenderingWidget RenderCanvas;
CameraComponent Camera;
Gizmo.Instance GizmoInstance;
GameObject _previewGO;
Terrain _previewTerrain;
TerrainStorage _previewStorage;
GameObject _splatOverlayGO;
ModelRenderer _splatOverlayRenderer;
Mesh _overlayMesh;
float[] _overlayTargetHeights;
float[] _overlayCurrentHeights;
Color32[] _overlayCurrentColors;
float[,] _overlaySplatmap;
bool _overlayUseSplatColors;
bool _overlayAnimating;
bool _overlayColorAnimating;
List<Color> _splatColorCache = new();
List<float> _currentFrameTimes;
List<float> _targetFrameTimes;
bool _gradientAnimating;
bool _isAnimatingGradient;
const float PreviewMorphSpeed = 8f;
const float MeshMorphSpeed = PreviewMorphSpeed * 0.25f;
float _orbitDistance = 20000f;
float _orbitAngle = 0f;
float _orbitPitch = 30f;
bool _autoSpin = true;
FloatSlider ZoomSlider;
const float SpinSpeed = 8f; // degrees per second
const int PreviewResolution = 512;
const float PreviewTerrainSize = 20000f;
const float PreviewTerrainHeight = 5000f;
SerializedObject _serialized;
bool _previewDirty;
float _lastPreviewRegen = float.MinValue;
bool _isGenerating = false;
int _generationToken = 0;
List<Widget> _domainWarpingWidgets = new();
List<Widget> _riverCarvingWidgets = new();
List<Widget> _stagingAreaWidgets = new();
Dictionary<string, Widget> _propsPages = new();
SegmentedControl _propsTabBar;
Widget _propsContent;
string _activePropsTab;
Widget _tilesContainer;
Button _randomizeMaterialsButton;
Label _materialLoadingLabel;
GradientControlWidget _gradientControlWidget;
WrapSelector ShapeArray;
WrapSelector CategoryArray;
public class DynamicEnum
{
private readonly Dictionary<string, int> _values = new Dictionary<string, int>();
private int _nextValue = 0;
public void Add( string name )
{
if ( !_values.ContainsKey( name ) )
{
_values[name] = _nextValue++;
}
}
public int GetValue( string name )
{
if ( string.IsNullOrEmpty( name ) ) return -1;
return _values.TryGetValue( name, out var value ) ? value : -1; // Return -1 if not found
}
public string GetName( int key )
{
return _values.FirstOrDefault( pair => pair.Value == key ).Key ?? "Unknown"; // Return "Unknown" if not found
}
public string[] GetNames()
{
return _values.Keys.ToArray();
}
}
public static string[] GetMethodsFromClass( string className )
{
// Attempt to get the Type from the class name (fully qualified)
Type classType = Type.GetType( className );
if ( classType == null )
{
throw new ArgumentException( $"Class '{className}' could not be found. Ensure the namespace is included." );
}
List<string> methodNames = new List<string>();
// Get all public methods (static and instance) from the class
MethodInfo[] methods = classType.GetMethods( BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static );
foreach ( var method in methods )
{
// Exclude methods not declared in this class
if ( method.DeclaringType == classType )
{
methodNames.Add( method.Name );
}
}
return methodNames.ToArray();
}
public string[] GetTerrainCategoryClasses( params Type[] CategoryClasses )
{
HashSet<string> methodNames = new HashSet<string>();
foreach ( Type CategoryClass in CategoryClasses )
{
// Get all public static methods from the class
MethodInfo[] methods = CategoryClass.GetMethods( BindingFlags.Public | BindingFlags.Static );
foreach ( MethodInfo method in methods )
{
// Exclude inherited methods or non-relevant ones
if ( method.DeclaringType == CategoryClass )
{
methodNames.Add( $"{CategoryClass.Name}.{method.Name}" );
TerrainCategoryEnum.Add( CategoryClass.Name);
TerrainCategoryArray.Add( CategoryClass.Name );
}
}
}
return methodNames.ToArray();
}
public string[] GetTerrainShapeMethods( Type shapeClass )
{
HashSet<string> methodNames = new HashSet<string>();
MethodInfo[] methods = shapeClass.GetMethods( BindingFlags.Public | BindingFlags.Static );
foreach ( MethodInfo method in methods )
{
// Exclude inherited methods or non-relevant ones
if ( method.DeclaringType == shapeClass )
{
//TerrainShapeArray.Add( shapeClass.Name );
//TerrainShapeEnum.Add( shapeClass.Name );
}
}
return methodNames.ToArray();
}
public static object CallMethod( string className, string methodName, object[] parameters = null )
{
// Get the class type
Type classType = Type.GetType( className );
if ( classType == null )
{
throw new ArgumentException( $"Class '{className}' could not be found." );
}
// Get the method info
MethodInfo method = classType.GetMethod( methodName, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance );
if ( method == null )
{
throw new ArgumentException( $"Method '{methodName}' could not be found in class '{className}'." );
}
// Check if the method is static or instance
object instance = null;
if ( !method.IsStatic )
{
instance = Activator.CreateInstance( classType );
}
// Invoke the method
object result = method.Invoke( instance, parameters );
// Ensure the return type is compatible
if ( result is not float )
{
throw new InvalidOperationException( $"Method '{methodName}' does not return a float." );
}
return result;
}
public void InitialShapes()
{
ShapeArray.DestroyChildren();
TerrainShapeArray.Clear();
string className = $"Sturnus.TerrainGenerationTool.Islands"; // Fully qualified name
string[] methods = GetMethodsFromClass( className );
// Print the methods
foreach ( string method in methods )
{
TerrainShapeArray.Add( method );
TerrainShapeEnum.Add( method );
}
foreach ( var shape in TerrainShapeArray )
{
ShapeArray.AddOption( shape );
}
}
public TerrainGenerationTool() : base()
{
WindowTitle = "Terrain Generation Tool";
SetWindowIcon( "terrain" );
MinimumSize = new Vector2( 1000, 700 );
Size = new Vector2( 1500, 900 );
StartCentered = true;
_serialized = this.GetSerialized();
_serialized.OnPropertyChanged += OnSerializedPropertyChanged;
string[] terrainCategoryClasses = GetTerrainCategoryClasses( terrainCategoryClassesTypes.ToArray() );
string[] terrainShapeMethods = GetTerrainShapeMethods( typeof(Islands) );
//Create TerrainGenerationTool folder if it doesn't exist.
Directory.CreateDirectory( GenerationPath );
SplatMapGradient.Blending = Gradient.BlendMode.Stepped;
Layout = Layout.Row();
Layout.Margin = 0;
Layout.Spacing = 0;
// ---------- Left: options panel ----------
var scroll = new ScrollArea( this );
scroll.Canvas = new Widget( scroll );
scroll.Canvas.Layout = Layout.Column();
scroll.Canvas.Layout.Margin = 10;
scroll.Canvas.Layout.Spacing = 5;
scroll.MinimumWidth = 400;
scroll.MaximumWidth = 480;
Layout.Add( scroll );
var body = scroll.Canvas.Layout;
// ---------- Left: grouped property tabs ----------
var propsRoot = body.Add( new Widget( null ), 1 );
propsRoot.Layout = Layout.Column();
propsRoot.Layout.Spacing = 5;
_propsTabBar = propsRoot.Layout.Add( new SegmentedControl() );
_propsTabBar.ShowText = true;
_propsTabBar.FixedHeight = Theme.RowHeight * 1.6f;
_propsTabBar.OnSelectedChanged += ( name ) => SelectPropsTab( name );
_propsContent = propsRoot.Layout.Add( new Widget( null ), 1 );
_propsContent.Layout = Layout.Column();
_propsContent.Layout.Margin = 0;
_propsContent.Layout.Alignment = TextFlag.Top;
// --- Terrain Type tab ---
var typePage = CreatePropsPage();
typePage.Layout.Add( new Label( "Terrain Dimensions" ) );
typePage.Layout.Add( new EnumControlWidget( _serialized.GetProperty( nameof( TerrainDimensionsEnum ) ) ) );
typePage.Layout.Add( new Label( "Terrain Category" ) );
CategoryArray = typePage.Layout.Add( new WrapSelector() );
for ( int i = 0; i < TerrainCategoryArray.ToArray().GetLength( 0 ); i++ )
{
List<string> rowValues = new List<string>();
rowValues.Add( TerrainCategoryArray.ToArray()[i] );
CategoryArray.AddOption( rowValues[0] );
}
typePage.Layout.Add( new Label( "Terrain Shape" ) );
ShapeArray = typePage.Layout.Add( new WrapSelector() );
InitialShapes();
CategoryArray.OnSelectedChanged += ( _ ) =>
{
RebuildShapes();
ApplySelectedCategory();
_previewDirty = true;
};
ShapeArray.OnSelectedChanged += ( _ ) =>
{
ApplySelectedShape();
_previewDirty = true;
};
if ( CategoryArray.Children.Count() > 0 )
{
CategoryArray.SelectedIndex = 0;
CategoryArray.Selected = CategoryArray.Children.First().Name;
}
RebuildShapes();
if ( ShapeArray.Children.Count() > 0 )
{
ShapeArray.SelectedIndex = 0;
ShapeArray.Selected = ShapeArray.Children.First().Name;
}
AddPropsTab( "Terrain Type", "terrain", typePage, "Terrain dimensions, category and shape" );
// --- Height / Scale tab ---
var heightPage = CreatePropsPage();
heightPage.Layout.Add( new Label( "Min Height (relative)" ) );
heightPage.Layout.Add( FloatSlider( nameof( TerrainMinHeight ) ) );
heightPage.Layout.Add( new Label( "Max Height (relative)" ) );
heightPage.Layout.Add( FloatSlider( nameof( TerrainMaxHeight ) ) );
heightPage.Layout.Add( new Label( "Terrain Plane Scale" ) );
heightPage.Layout.Add( FloatSlider( nameof( TerrainPlaneScale ) ) );
heightPage.Layout.Add( new Label( "Terrain Seed" ) );
var seedRow = heightPage.Layout.AddRow();
seedRow.Spacing = 4;
var seedControl = seedRow.Add( new IntegerControlWidget( _serialized.GetProperty( nameof( TerrainSeed ) ) ), 1 );
seedRow.Add( new IconButton( "casino", RandomizeSeed, this )
{
ToolTip = "Randomize seed",
IconSize = 16,
FixedSize = new Vector2( 26, 26 )
} );
AddPropsTab( "Height/Scale", "straighten", heightPage, "Terrain height, plane scale and seed" );
// --- Smooth / Noise tab ---
var noisePage = CreatePropsPage();
noisePage.Layout.Add( new Label( "Smoothing Passes" ) );
noisePage.Layout.Add( IntSlider( nameof( SmoothingPasses ) ) );
noisePage.Layout.Add( new Label( "Noise Layer Stacks" ) );
noisePage.Layout.Add( IntSlider( nameof( NoiseLayerStacks ) ) );
AddPropsTab( "Smooth/Noise", "grain", noisePage, "Terrain smoothing and noise layers" );
// --- Splat tab ---
var splatPage = CreatePropsPage();
splatPage.Layout.Add( new Label( "Splat Layer Count" ) );
splatPage.Layout.Add( IntSlider( nameof( SplatLayerCount ) ) );
splatPage.Layout.Add( new Label( "Splat Map Count" ) );
splatPage.Layout.Add( IntSlider( nameof( SplatMapCount ) ) );
splatPage.Layout.Add( new Label( "Dispersion" ) );
splatPage.Layout.Add( new EnumControlWidget( _serialized.GetProperty( nameof( SplatDispersion ) ) ) );
splatPage.Layout.Add( new Label( "Blend Strength" ) );
splatPage.Layout.Add( FloatSlider( nameof( SplatBlendStrength ) ) );
splatPage.Layout.Add( new Label( "Splatmap Colors/Threshold" ) );
_gradientControlWidget = new GradientControlWidget( _serialized.GetProperty( nameof( SplatMapGradient ) ) );
splatPage.Layout.Add( _gradientControlWidget );
splatPage.Layout.Add( new Label( "Preview Materials" ) );
splatPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( PreviewSplatMaterials ) ) ) );
var materialHint = new Label( "Assigns random local .tmat terrain materials from your project's assets to the splat layers so you can preview the material blending on the terrain." );
materialHint.SetStyles( "font-size: 10px; color: #888;" );
materialHint.WordWrap = true;
materialHint.MaximumWidth = 260;
splatPage.Layout.Add( materialHint );
var randomizeRow = splatPage.Layout.AddRow();
_randomizeMaterialsButton = randomizeRow.Add( new Button( "Randomize Materials", "casino" ) );
_randomizeMaterialsButton.Clicked += RandomizeMaterials;
_materialLoadingLabel = randomizeRow.Add( new Label( "Loading..." ) );
_materialLoadingLabel.SetStyles( "font-size: 10px; color: #888;" );
_materialLoadingLabel.Visible = false;
_materialLoadingLabel.WordWrap = true;
_materialLoadingLabel.MaximumWidth = 120;
AddPropsTab( "Splat", "palette", splatPage, "Splatmap layers, maps, colors and dispersion" );
// --- Warping tab ---
var warpPage = CreatePropsPage();
warpPage.Layout.Add( new Label( "Domain Warping" ) );
warpPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( DomainWarping ) ) ) );
var DomainWarpingSizeLabel = warpPage.Layout.Add( new Label( "Domain Warping (Size)" ) );
var DomainWarpingSizeFloat = warpPage.Layout.Add( FloatSlider( nameof( DomainWarpingSize ) ) );
var DomainWarpingStrengthLabel = warpPage.Layout.Add( new Label( "Domain Warping (Strength)" ) );
var DomainWarpingStrengthFloat = warpPage.Layout.Add( FloatSlider( nameof( DomainWarpingStrength ) ) );
_domainWarpingWidgets.AddRange( new Widget[] { DomainWarpingSizeLabel, DomainWarpingSizeFloat, DomainWarpingStrengthLabel, DomainWarpingStrengthFloat } );
AddPropsTab( "Warping", "blur_on", warpPage, "Domain warping options" );
// --- River tab ---
var riverPage = CreatePropsPage();
riverPage.Layout.Add( new Label( "River Carving" ) );
riverPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( RiverCarvingBool ) ) ) );
var RiverCarvingFrequencyLabel = riverPage.Layout.Add( new Label( "RiverCarvingFrequency" ) );
var RiverCarvingFrequencyFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingFrequency ) ) );
/*var RiverCarvingStrength = riverPage.Layout.Add( new Label("RiverCarvingStrength"));
var RiverCarvingStrengthFloat = riverPage.Layout.Add( FloatSlider( nameof(RiverCarvingStrength) ) );*/
var RiverCarvingDepthLabel = riverPage.Layout.Add( new Label( "RiverCarvingDepth" ) );
var RiverCarvingDepthFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingDepth ) ) );
var RiverCarvingWidthLabel = riverPage.Layout.Add( new Label( "RiverCarvingWidth" ) );
var RiverCarvingWidthFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingWidth ) ) );
var RiverCarvingSpacingLabel = riverPage.Layout.Add( new Label( "RiverCarvingSpacing" ) );
var RiverCarvingSpacingFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingSpacing ) ) );
var RiverCarvingTurbulenceStrengthLabel = riverPage.Layout.Add( new Label( "RiverCarvingTurbulenceStrength" ) );
var RiverCarvingTurbulenceStrengthFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingTurbulenceStrength ) ) );
var RiverCarvingTurbulenceFrequencyLabel = riverPage.Layout.Add( new Label( "RiverCarvingTurbulenceFrequency" ) );
var RiverCarvingTurbulenceFrequencyFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingTurbulenceFrequency ) ) );
_riverCarvingWidgets.AddRange( new Widget[]
{
RiverCarvingFrequencyLabel, RiverCarvingFrequencyFloat,
/*RiverCarvingStrength, RiverCarvingStrengthFloat,*/
RiverCarvingDepthLabel, RiverCarvingDepthFloat,
RiverCarvingWidthLabel, RiverCarvingWidthFloat,
RiverCarvingSpacingLabel, RiverCarvingSpacingFloat,
RiverCarvingTurbulenceStrengthLabel, RiverCarvingTurbulenceStrengthFloat,
RiverCarvingTurbulenceFrequencyLabel, RiverCarvingTurbulenceFrequencyFloat
} );
AddPropsTab( "River", "water", riverPage, "River carving options" );
// --- Staging tab ---
var stagingPage = CreatePropsPage();
stagingPage.Layout.Add( new Label( "Staging Area" ) );
stagingPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( StagingArea ) ) ) );
var StagingAreaSizeLabel = stagingPage.Layout.Add( new Label( "Staging Area (Size)" ) );
var StagingAreaSizeFloat = stagingPage.Layout.Add( IntSlider( nameof( StagingAreaSize ) ) );
var StagingAreaHeightLabel = stagingPage.Layout.Add( new Label( "Staging Area (Height)" ) );
var StagingAreaHeightFloat = stagingPage.Layout.Add( FloatSlider( nameof( StagingAreaHeight ) ) );
var StagingAreaXLabel = stagingPage.Layout.Add( new Label( "Staging Area (X)" ) );
var StagingAreaXFloat = stagingPage.Layout.Add( FloatSlider( nameof( StagingAreaX ) ) );
var StagingAreaYLabel = stagingPage.Layout.Add( new Label( "Staging Area (Y)" ) );
var StagingAreaYFloat = stagingPage.Layout.Add( FloatSlider( nameof( StagingAreaY ) ) );
_stagingAreaWidgets.AddRange( new Widget[]
{
StagingAreaSizeLabel, StagingAreaSizeFloat,
StagingAreaHeightLabel, StagingAreaHeightFloat,
StagingAreaXLabel, StagingAreaXFloat,
StagingAreaYLabel, StagingAreaYFloat
} );
AddPropsTab( "Staging", "square_foot", stagingPage, "Staging area placement" );
body.AddSpacingCell( 5 );
// ---------- Tile grid section (docked between props and actions) ----------
var tileGridSection = body.Add( new Widget( null ) );
tileGridSection.Layout = Layout.Column();
tileGridSection.Layout.Spacing = 4;
tileGridSection.Layout.Add( new Label( "Tile Grid" ) );
tileGridSection.Layout.Add( IntSlider( nameof( TerrainGridSize ) ) );
tileGridSection.Layout.Add( new Label( "Storage" ) );
tileGridSection.Layout.Add( new EnumControlWidget( _serialized.GetProperty( nameof( GridStorage ) ) ) );
var tileGridHint = new Label( "Click a tile to select which cell the Terrain Type category and shape apply to. Click 'All' to set every tile at once." );
tileGridHint.SetStyles( "font-size: 10px; color: #888;" );
tileGridHint.WordWrap = true;
tileGridHint.MaximumWidth = 260;
tileGridSection.Layout.Add( tileGridHint );
_tilesContainer = new Widget( null );
_tilesContainer.Layout = Layout.Column();
_tilesContainer.Layout.Spacing = 4;
tileGridSection.Layout.Add( _tilesContainer );
body.AddSpacingCell( 5 );
var GenerateButton = body.Add( new Button.Primary( "Generate", "auto_awesome", this ) );
var ExportButton = body.Add( new Button( "Export", "file_download", this ) );
ExportButton.Tint = "#41AF20";
var ApplyButton = body.Add( new Button( "Apply To Terrain", "file_upload", this ) );
ApplyButton.Tint = "#AF2020";
if ( _heightmap == null )
{
ExportButton.Enabled = false;
ApplyButton.Enabled = false;
}
GenerateButton.Clicked += () =>
{
BuildSplatColors();
int fullRes = (int)TerrainDimensionsEnum;
if ( GridStorage == GridStorageMode.PerCell )
{
// Each cell is its own full-resolution map - no stitching.
_cellHeightmaps = BuildPerCellHeightmaps(
fullRes, fullRes,
(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(),
(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),
(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),
(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),
(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),
RiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,
RiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,
StagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );
if ( _cellHeightmaps.Count == 0 )
{
Log.Error( "No per-cell heightmaps generated. Aborting." );
return;
}
_cellSplatmaps = BuildPerCellSplatmaps( _cellHeightmaps,
(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );
// A stitched preview map so the 3D preview still shows the whole grid tiled.
_heightmap = BuildHeightmap(
fullRes, fullRes,
(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(), TerrainGridSize,
(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),
(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),
(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),
(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),
RiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,
RiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,
StagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );
}
else
{
_heightmap = BuildHeightmap(
fullRes, fullRes,
(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(), TerrainGridSize,
(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),
(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),
(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),
(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),
RiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,
RiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,
StagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );
_cellHeightmaps = null;
_cellSplatmaps = null;
}
if ( _heightmap == null )
{
Log.Error( "Heightmap is not generated. Aborting." );
return;
}
_splatmap = BuildTileGridSplatmap( _heightmap, TerrainGridSize,
(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );
// Write the preview files for the asset folder, and build fresh textures for the widgets
GeneratePreviewFile( GenerationPath, out var previewBitmap, out var splatBitmap );
_preview_image_texture = TextureFromBitmap( previewBitmap );
PreviewImage.Texture = _preview_image_texture;
_preview_splatmap_texture = TextureFromBitmap( splatBitmap );
PreviewSplatmap.Texture = _preview_splatmap_texture;
ExportButton.Enabled = true;
ApplyButton.Enabled = true;
UpdatePreviewTerrain( _heightmap );
};
ExportButton.Clicked += () =>
{
GenerateImageFiles( ExportPath );
var PopUp = new PopupWindow( "Export Complete", $"Files exported to {ExportPath}", "Okay" );
PopUp.Show();
};
ApplyButton.Clicked += () =>
{
IDictionary<string, Action> WarnDiaglog = new Dictionary<string, Action>(); ;
WarnDiaglog.Add( "Apply", GridStorage == GridStorageMode.PerCell ? UpdatePerCellTerrains : UpdateTerrain );
var PopUpWarn = new PopupWindow( "Warning: Terrain Override", "This will override your current scene's terrain data.","Cancel", WarnDiaglog );
PopUpWarn.Show();
};
body.AddStretchCell();
// ---------- Right: tabbed preview panel ----------
var rightPanel = Layout.Add( new Widget( null ), 1 );
rightPanel.Layout = Layout.Column();
rightPanel.Layout.Spacing = 0;
var PreviewTabs = rightPanel.Layout.Add( new VerticalTabWidget( this ), 1 );
PreviewTabs.StateCookie = "TerrainGenerationTool.PreviewTabs";
RenderCanvas = new SceneRenderingWidget( this );
RenderCanvas.OnPreFrame += OnPreFrame;
RenderCanvas.FocusMode = FocusMode.Click;
RenderCanvas.Scene = Scene.CreateEditorScene();
RenderCanvas.Scene.SceneWorld.AmbientLightColor = Color.FromBytes( 135, 206, 235 ) * 0.45f;
// 3D preview tab
PreviewTabs.AddPage( "3D Preview", "landscape", RenderCanvas, "3D terrain preview" );
// Height/Color maps tab
var mapsPage = new Widget( null );
mapsPage.Layout = Layout.Row();
mapsPage.Layout.Spacing = 5;
var _image_preview = new Editor.TextureWidget();
_image_preview.Texture = _preview_image_texture;
_image_preview.Size = new Vector2( 512, 512 );
PreviewImage = mapsPage.Layout.Add( _image_preview, 50 );
var _splatmap_preview = new Editor.TextureWidget();
_splatmap_preview.Texture = _preview_splatmap_texture;
_splatmap_preview.Size = new Vector2( 512, 512 );
PreviewSplatmap = mapsPage.Layout.Add( _splatmap_preview, 50 );
PreviewTabs.AddPage( "Height/Color Maps", "grid_view", mapsPage, "Heightmap and splatmap preview" );
using ( RenderCanvas.Scene.Push() )
{
Camera = new GameObject( true, "camera" ).GetOrAddComponent<CameraComponent>( false );
Camera.BackgroundColor = Color.FromBytes( 135, 206, 235 );
Camera.ZFar = 100000;
Camera.Enabled = true;
PositionCameraForOrbit();
RenderCanvas.Camera = Camera;
var sun = new GameObject( true, "sun" ).GetOrAddComponent<DirectionalLight>( false );
sun.WorldRotation = Rotation.From( 45, 45, 0 );
sun.LightColor = Color.White;
sun.SkyColor = Color.FromBytes( 135, 206, 235 );
sun.Enabled = true;
var sun2 = new GameObject( true, "sun2" ).GetOrAddComponent<DirectionalLight>( false );
sun2.WorldRotation = Rotation.From( -30, 135, 0 );
sun2.LightColor = Color.White * 0.3f;
sun2.SkyColor = Color.FromBytes( 135, 206, 235 );
sun2.Enabled = true;
}
GizmoInstance = RenderCanvas.GizmoInstance;
// Create the preview terrain - a real Terrain component in the preview scene
using ( RenderCanvas.Scene.Push() )
{
_previewGO = new GameObject( true, "terrain preview" );
_previewTerrain = _previewGO.AddComponent<Terrain>( false );
_previewStorage = new TerrainStorage();
_previewStorage.EmbeddedResource = new Sandbox.Resources.EmbeddedResource { ResourceCompiler = "embed" };
_previewStorage.SetResolution( PreviewResolution );
_previewStorage.TerrainSize = PreviewTerrainSize;
_previewStorage.TerrainHeight = PreviewTerrainHeight;
_previewTerrain.Storage = _previewStorage;
_previewTerrain.TerrainSize = PreviewTerrainSize;
_previewTerrain.TerrainHeight = PreviewTerrainHeight;
// Terrain spans [0, TerrainSize] from its origin - shift it so it's centered on the origin
_previewGO.WorldPosition = new Vector3( -PreviewTerrainSize * 0.5f, -PreviewTerrainSize * 0.5f, 0f );
_previewTerrain.Enabled = true;
// Overlay mesh that shows the splatmap colors when the material preview is off.
// Slightly offset above the terrain so it doesn't z-fight with the terrain surface.
// Parented to the terrain GO (which is centered at origin), so the mesh uses local coords.
_splatOverlayGO = new GameObject( true, "splat overlay" );
_splatOverlayGO.Parent = _previewGO;
_splatOverlayGO.LocalPosition = new Vector3( 0, 0, 1f );
_splatOverlayRenderer = _splatOverlayGO.AddComponent<ModelRenderer>();
_splatOverlayRenderer.MaterialOverride = Material.Load( "materials/default/vertex_color.vmat" );
_splatOverlayGO.Enabled = false;
}
// Zoom slider at the bottom of the preview panel
var zoomRow = rightPanel.Layout.AddRow();
zoomRow.Margin = new Sandbox.UI.Margin( 8, 4, 8, 6 );
zoomRow.Spacing = 8;
zoomRow.Add( new IconButton( "zoom_out", () => ZoomSlider.Value = MathF.Max( ZoomSlider.Minimum, ZoomSlider.Value - 500f ), this ) { IconSize = 16, FixedSize = new Vector2( 22, 22 ) } );
ZoomSlider = zoomRow.Add( new FloatSlider( this ), 1 );
ZoomSlider.Minimum = 10000f;
ZoomSlider.Maximum = 40000f;
ZoomSlider.Step = 500f;
ZoomSlider.Value = 40000f - _orbitDistance + 10000f;
ZoomSlider.OnValueEdited = UpdateOrbitFromZoom;
zoomRow.Add( new IconButton( "zoom_in", () => ZoomSlider.Value = MathF.Min( ZoomSlider.Maximum, ZoomSlider.Value + 500f ), this ) { IconSize = 16, FixedSize = new Vector2( 22, 22 ) } );
ApplyConditionalVisibility();
RebuildTileGridUI();
LoadFacepunchMaterialsAsync();
RegeneratePreview();
Show();
}
void OnSerializedPropertyChanged( SerializedProperty prop )
{
// Intermediate frames written during gradient animation shouldn't retrigger a regen
if ( !_isAnimatingGradient )
_previewDirty = true;
if ( prop is null ) return;
switch ( prop.Name )
{
case nameof( TerrainGridSize ):
RebuildTileGridUI();
break;
case nameof( GridStorage ):
// Reset stored per-cell maps when the storage mode changes so stale data isn't applied/exported
_cellHeightmaps = null;
_cellSplatmaps = null;
break;
case nameof( TerrainMinHeight ):
if ( !_syncingTileSelectors ) WriteSelectedValues( minHeight: TerrainMinHeight );
break;
case nameof( TerrainMaxHeight ):
if ( !_syncingTileSelectors ) WriteSelectedValues( maxHeight: TerrainMaxHeight );
break;
case nameof( TerrainPlaneScale ):
if ( !_syncingTileSelectors ) WriteSelectedValues( planeScale: TerrainPlaneScale );
break;
case nameof( TerrainSeed ):
if ( !_syncingTileSelectors ) WriteSelectedValues( seed: TerrainSeed );
break;
case nameof( SmoothingPasses ):
if ( !_syncingTileSelectors ) WriteSelectedValues( smoothing: SmoothingPasses );
break;
case nameof( NoiseLayerStacks ):
if ( !_syncingTileSelectors ) WriteSelectedValues( noiseLayers: NoiseLayerStacks );
break;
case nameof( DomainWarping ):
SetWidgetsVisible( _domainWarpingWidgets, DomainWarping );
if ( !_syncingTileSelectors ) WriteSelectedValues( warp: DomainWarping );
break;
case nameof( DomainWarpingSize ):
if ( !_syncingTileSelectors ) WriteSelectedValues( warpSize: DomainWarpingSize );
break;
case nameof( DomainWarpingStrength ):
if ( !_syncingTileSelectors ) WriteSelectedValues( warpStrength: DomainWarpingStrength );
break;
case nameof( StagingArea ):
SetWidgetsVisible( _stagingAreaWidgets, StagingArea );
break;
case nameof( SplatLayerCount ):
case nameof( SplatDispersion ):
case nameof( SplatBlendStrength ):
case nameof( SplatMapCount ):
if ( !_syncingTileSelectors )
{
WriteSelectedValues(
splatLayers: prop.Name == nameof( SplatLayerCount ) ? SplatLayerCount : (int?)null,
splatMaps: prop.Name == nameof( SplatMapCount ) ? SplatMapCount : (int?)null,
splatDispersion: prop.Name == nameof( SplatDispersion ) ? SplatDispersion : (SplatDispersionMode?)null,
splatBlend: prop.Name == nameof( SplatBlendStrength ) ? SplatBlendStrength : (float?)null );
}
// Resample the gradient into evenly spaced stops so the colors/thresholds match the layer count
ResampleSplatGradient();
if ( PreviewSplatMaterials )
{
_previewMaterials = null;
RandomizeMaterialsAsync();
}
break;
case nameof( SplatMapGradient ):
// The user edited the gradient colors in the widget - make that the source of
// truth so later resamples keep their colors instead of falling back to the
// stale random cache.
if ( !_isAnimatingGradient )
{
_splatColorCache.Clear();
if ( SplatMapGradient.Colors != null )
{
foreach ( var frame in SplatMapGradient.Colors )
_splatColorCache.Add( frame.Value );
}
}
break;
case nameof( PreviewSplatMaterials ):
if ( PreviewSplatMaterials && ( _previewMaterials == null || _previewMaterials.Length < Math.Max( SplatLayerCount, 2 ) ) )
{
RandomizeMaterials();
}
if ( !PreviewSplatMaterials )
{
_previewMaterials = null;
}
break;
}
}
/// <summary>
/// The largest splat layer count across all tiles (so shared resources like the preview
/// material list and gradient cover every tile). Falls back to the global value.
/// </summary>
int MaxTileSplatLayers()
{
int max = Math.Max( SplatLayerCount, 2 );
if ( _tileSplatLayerCounts != null )
{
foreach ( var lc in _tileSplatLayerCounts )
max = Math.Max( max, lc );
}
return max;
}
void ResampleSplatGradient()
{
int layerCount = MaxTileSplatLayers();
// Seed the persistent color cache from the current gradient first so user edits are kept.
if ( _splatColorCache.Count == 0 && SplatMapGradient.Colors != null && SplatMapGradient.Colors.Count() > 0 )
{
foreach ( var frame in SplatMapGradient.Colors )
_splatColorCache.Add( frame.Value );
}
// Add new colors to the end as layers grow - existing colors keep their index.
while ( _splatColorCache.Count < layerCount )
_splatColorCache.Add( RandomBrightColor() );
// Compute the target stop positions.
float[] thresholds;
var source = _previewHeightmap ?? _heightmap;
if ( SplatDispersion == SplatDispersionMode.Natural && source != null )
{
thresholds = ComputeNaturalThresholds( source, layerCount );
}
else
{
thresholds = new float[layerCount];
for ( int i = 0; i < layerCount; i++ )
{
thresholds[i] = (float)i / (layerCount - 1);
}
}
_splatthresholds = thresholds;
// First build: set the gradient directly.
if ( _currentFrameTimes is null )
{
_currentFrameTimes = thresholds.ToList();
_targetFrameTimes = thresholds.ToList();
ApplyGradientFromFrames();
return;
}
int oldCount = _currentFrameTimes.Count;
// New frames (added layers) enter from the right and slide left to their target.
if ( layerCount > oldCount )
{
for ( int i = oldCount; i < layerCount; i++ )
_currentFrameTimes.Add( 1f );
_targetFrameTimes = thresholds.ToList();
}
// Removed frames slide out to the right (target 1.0) and get dropped when they arrive.
else if ( layerCount < oldCount )
{
// Existing frames keep their current positions; the extra ones head right.
var newTargets = thresholds.ToList();
while ( newTargets.Count < oldCount )
newTargets.Add( 1f );
_targetFrameTimes = newTargets;
}
// Same count - just retarget the existing frames.
else
{
_targetFrameTimes = thresholds.ToList();
}
_gradientAnimating = true;
}
void ApplyGradientFromFrames()
{
// Only include frames that are still "in play" (haven't slid off the right edge yet).
var frames = new List<Gradient.ColorFrame>();
for ( int i = 0; i < _currentFrameTimes.Count && i < _splatColorCache.Count; i++ )
{
float t = Math.Clamp( _currentFrameTimes[i], 0f, 1f );
frames.Add( new Gradient.ColorFrame( t, _splatColorCache[i] ) );
}
SplatMapGradient = new Gradient( frames.ToArray() );
SplatMapGradient.Blending = Gradient.BlendMode.Stepped;
_isAnimatingGradient = true;
try
{
_serialized.GetProperty( nameof( SplatMapGradient ) )?.SetValue( SplatMapGradient );
}
finally
{
_isAnimatingGradient = false;
}
}
/// <summary>
/// Eases the gradient color stops toward their target positions so palette changes
/// slide in/out on the scale instead of snapping.
/// </summary>
void UpdateGradientAnimation()
{
if ( !_gradientAnimating || _currentFrameTimes is null || _targetFrameTimes is null ) return;
float t = 1f - MathF.Exp( -PreviewMorphSpeed * RealTime.Delta );
float maxDelta = 0f;
for ( int i = 0; i < _currentFrameTimes.Count && i < _targetFrameTimes.Count; i++ )
{
float delta = _targetFrameTimes[i] - _currentFrameTimes[i];
_currentFrameTimes[i] += delta * t;
maxDelta = MathF.Max( maxDelta, MathF.Abs( delta ) );
}
// Drop frames that have slid off the right edge (removed layers)
if ( _currentFrameTimes.Count > _targetFrameTimes.Count )
{
while ( _currentFrameTimes.Count > _targetFrameTimes.Count )
{
int last = _currentFrameTimes.Count - 1;
if ( _currentFrameTimes[last] >= 0.999f )
{
_currentFrameTimes.RemoveAt( last );
if ( _splatColorCache.Count > _targetFrameTimes.Count )
_splatColorCache.RemoveAt( _splatColorCache.Count - 1 );
}
else break;
}
}
ApplyGradientFromFrames();
// Force the gradient widget to repaint this frame so the motion is smooth
if ( _gradientControlWidget != null && _gradientControlWidget.IsValid() )
_gradientControlWidget.Update();
// The overlay mesh samples the live gradient, so rebuild it while the palette animates
BuildOverlayMesh();
if ( maxDelta < 0.001f )
{
_currentFrameTimes = _targetFrameTimes.ToList();
_gradientAnimating = false;
}
}
static Color RandomBrightColor()
{
// Pick a hue at random, keep saturation/value high so it stands out
float hue = Random.Shared.NextSingle() * 360f;
return new ColorHsv( hue, 0.8f, 1.0f ).ToColor();
}
void SetWidgetsVisible( List<Widget> widgets, bool visible )
{
foreach ( var widget in widgets )
{
widget.Visible = visible;
widget.Enabled = visible;
}
}
Widget CreatePropsPage()
{
var page = new Widget( null );
page.VerticalSizeMode = SizeMode.CanShrink;
page.HorizontalSizeMode = SizeMode.Flexible;
page.Layout = Layout.Column();
page.Layout.Margin = 10;
page.Layout.Spacing = 5;
page.Layout.Alignment = TextFlag.Top;
return page;
}
FloatControlWidget FloatSlider( string propertyName )
{
var property = _serialized.GetProperty( propertyName );
var control = new FloatControlWidget( property );
MakeRanged( control, property );
return control;
}
IntegerControlWidget IntSlider( string propertyName )
{
var property = _serialized.GetProperty( propertyName );
var control = new IntegerControlWidget( property );
MakeRanged( control, property );
return control;
}
void MakeRanged( FloatControlWidget control, SerializedProperty property )
{
if ( property is null ) return;
property.TryGetAttribute<MinMaxAttribute>( out var minMax );
if ( minMax is null ) return;
float step = 0.01f;
if ( property.TryGetAttribute<StepAttribute>( out var stepAttr ) )
{
step = stepAttr.Step;
}
control.MakeRanged( new Vector2( minMax.MinValue, minMax.MaxValue ), step, true, true );
}
void RandomizeSeed()
{
var property = _serialized.GetProperty( nameof( TerrainSeed ) );
if ( property is null ) return;
TerrainSeed = Random.Shared.NextInt64();
property.SetValue( TerrainSeed );
_previewDirty = true;
}
void LoadFacepunchMaterialsAsync()
{
try
{
// Find all local tmat assets in the project's assets folder (not cloud ones).
// Prefer 1K variants when a material has multiple resolutions.
var allLocal = Editor.AssetSystem.All
.Where( a => a is not null && !a.IsDeleted && !a.IsCloud )
.Where( a => (a.RelativePath?.EndsWith( ".tmat" ) ?? false) )
.ToList();
var with1k = allLocal.Where( a => a.RelativePath.Contains( "_1k" ) ).ToList();
_localTmatAssets = with1k.Count > 0 ? with1k : allLocal;
if ( _localTmatAssets.Count == 0 )
Log.Warning( "No local .tmat terrain materials found in the project's assets folder" );
}
catch ( System.Exception e )
{
Log.Error( $"Failed to find local terrain materials: {e.Message}" );
}
}
void RandomizeMaterials()
{
if ( !PreviewSplatMaterials ) return;
if ( _localTmatAssets is null || _localTmatAssets.Count == 0 )
{
// Load the local tmat list first, then randomize once it's available
_ = LoadFacepunchMaterialsAndRandomize();
return;
}
RandomizeMaterialsAsync();
}
async Task LoadFacepunchMaterialsAndRandomize()
{
try
{
// Find all local tmat assets in the project's assets folder (not cloud ones).
// Prefer 1K variants when a material has multiple resolutions.
var allLocal = Editor.AssetSystem.All
.Where( a => a is not null && !a.IsDeleted && !a.IsCloud )
.Where( a => (a.RelativePath?.EndsWith( ".tmat" ) ?? false) )
.ToList();
var with1k = allLocal.Where( a => a.RelativePath.Contains( "_1k" ) ).ToList();
_localTmatAssets = with1k.Count > 0 ? with1k : allLocal;
if ( PreviewSplatMaterials && _localTmatAssets.Count > 0 )
RandomizeMaterialsAsync();
}
catch ( System.Exception e )
{
Log.Error( $"Failed to find local terrain materials: {e.Message}" );
}
}
async void RandomizeMaterialsAsync()
{
if ( _materialLoadingLabel != null )
{
_materialLoadingLabel.Text = "Loading materials...";
_materialLoadingLabel.Visible = true;
}
int layerCount = MaxTileSplatLayers();
int gen = ++_previewMaterialsGeneration;
try
{
var pool = new List<Editor.Asset>( _localTmatAssets );
var materials = new List<TerrainMaterial>();
// Pull from the pool until we have layerCount usable materials (skipping any
// whose textures fail to compile) or the pool runs out.
while ( materials.Count < layerCount && pool.Count > 0 )
{
int idx = Random.Shared.Next( pool.Count );
var asset = pool[idx];
pool.RemoveAt( idx );
if ( !asset.TryLoadResource<TerrainMaterial>( out var found ) || found is null )
{
Log.Warning( $"Failed to load TerrainMaterial from '{asset.Path}'" );
continue;
}
// Only accept materials whose compiled BCR/NHO textures actually exist -
// otherwise the terrain renders a pink checkerboard.
if ( !IsMaterialUsable( found, asset.Path ) )
continue;
materials.Add( found );
}
if ( gen != _previewMaterialsGeneration ) return;
if ( materials.Count == 0 )
{
_previewMaterials = null;
Log.Warning( "No local terrain materials could be loaded" );
}
else
{
_previewMaterials = materials.ToArray();
}
_previewDirty = true;
}
catch ( System.Exception e )
{
Log.Error( $"Failed to load preview materials: {e.Message}" );
}
finally
{
if ( gen == _previewMaterialsGeneration && _materialLoadingLabel != null )
_materialLoadingLabel.Visible = false;
}
}
/// <summary>
/// Checks that a terrain material's compiled BCR/NHO textures are usable. The terrain shader
/// samples these bindlessly, and a missing/failed compile shows up as a pink checkerboard.
/// </summary>
bool IsMaterialUsable( TerrainMaterial material, string ident )
{
if ( material is null ) return false;
try
{
var bcr = material.BCRTexture;
if ( bcr is null || bcr.IsError || !bcr.IsValid )
{
Log.Warning( $"Skipping '{ident}': BCR texture missing or failed to compile" );
return false;
}
var nho = material.NHOTexture;
if ( nho is null || nho.IsError || !nho.IsValid )
{
Log.Warning( $"Skipping '{ident}': NHO texture missing or failed to compile" );
return false;
}
return true;
}
catch ( System.Exception e )
{
Log.Warning( $"Skipping '{ident}': {e.Message}" );
return false;
}
}
void AddPropsTab( string name, string icon, Widget page, string tooltip )
{
_propsTabBar.AddOption( name, icon );
_propsPages[name] = page;
_propsContent.Layout.Add( page );
page.Visible = false;
page.ToolTip = tooltip;
if ( _propsPages.Count == 1 )
{
SelectPropsTab( name );
}
}
void SelectPropsTab( string name )
{
foreach ( var entry in _propsPages )
{
entry.Value.Visible = entry.Key == name;
}
if ( _activePropsTab != name )
{
_activePropsTab = name;
_previewDirty = true;
}
}
void ApplyConditionalVisibility()
{
SetWidgetsVisible( _domainWarpingWidgets, DomainWarping );
SetWidgetsVisible( _riverCarvingWidgets, RiverCarvingBool );
SetWidgetsVisible( _stagingAreaWidgets, StagingArea );
}
[EditorEvent.Frame]
public void FrameUpdate()
{
// Tick the preview scene so the terrain clipmap builds and updates
if ( RenderCanvas != null && RenderCanvas.Scene.IsValid() )
RenderCanvas.Scene.EditorTick( RealTime.Now, RealTime.Delta );
// Morph the overlay mesh toward its target shape every frame
UpdateOverlayAnimation();
// If a tile was just selected/deselected, keep rebuilding the overlay mesh so the
// grey-out smoothly eases in until the colors settle.
if ( _overlayColorAnimating )
{
BuildOverlayMesh();
}
// Animate the gradient color stops sliding in/out
UpdateGradientAnimation();
if ( !_previewDirty ) return;
if ( RealTime.Now - _lastPreviewRegen < 0.1f ) return;
if ( _isGenerating ) return;
_previewDirty = false;
_lastPreviewRegen = RealTime.Now;
RegeneratePreviewAsync();
}
async void RegeneratePreviewAsync()
{
if ( _previewTerrain is null || !_previewTerrain.IsValid() ) return;
if ( string.IsNullOrEmpty( CategoryArray?.Selected ) || string.IsNullOrEmpty( ShapeArray?.Selected ) ) return;
if ( _isGenerating ) return;
_isGenerating = true;
int token = ++_generationToken;
// Splat colors are read on the main thread into the shared arrays
BuildSplatColors();
// Snapshot UI-driven values on the main thread so the background task doesn't touch widgets
var tileCategories = (string[])_tileCategories.Clone();
var tileShapes = (string[])_tileShapes.Clone();
var tileSeeds = (long[])_tileSeeds.Clone();
var tileMinHeights = (float[])_tileMinHeights.Clone();
var tileMaxHeights = (float[])_tileMaxHeights.Clone();
var tilePlaneScales = (float[])_tilePlaneScales.Clone();
var tileSmoothing = (int[])_tileSmoothingPasses.Clone();
var tileNoiseLayers = (int[])_tileNoiseLayerStacks.Clone();
var tileWarping = (bool[])_tileDomainWarping.Clone();
var tileWarpingSizes = (float[])_tileDomainWarpingSizes.Clone();
var tileWarpingStrengths = (float[])_tileDomainWarpingStrengths.Clone();
var tileSplatLayerCounts = (int[])_tileSplatLayerCounts.Clone();
var tileSplatDispersions = (SplatDispersionMode[])_tileSplatDispersions.Clone();
var tileSplatBlends = (float[])_tileSplatBlendStrengths.Clone();
int gridSize = TerrainGridSize;
bool rivers = RiverCarvingBool;
float riverFrequency = RiverCarvingFrequency;
float riverWidth = RiverCarvingWidth;
float riverDepth = RiverCarvingDepth;
float riverTurbFreq = RiverCarvingTurbulenceFrequency;
float riverTurbStrength = RiverCarvingTurbulenceStrength;
float riverSpacing = RiverCarvingSpacing;
bool staging = StagingArea;
int stagingSize = StagingAreaSize;
float stagingHeight = StagingAreaHeight;
float stagingX = StagingAreaX;
float stagingY = StagingAreaY;
try
{
// CPU-heavy work (noise, smoothing, rivers, splatmap) runs off the main thread
float[,] heightmap = await Task.Run( () => BuildHeightmap(
PreviewResolution, PreviewResolution,
tileCategories, tileShapes, gridSize,
tileSeeds, tileNoiseLayers, tileMinHeights, tileMaxHeights,
tileWarping, tileWarpingSizes, tileWarpingStrengths,
tileSmoothing, tilePlaneScales,
rivers, riverFrequency, riverWidth, riverDepth,
riverTurbFreq, riverTurbStrength, riverSpacing,
staging, stagingSize, stagingHeight, stagingX, stagingY ) );
if ( token != _generationToken ) return;
if ( heightmap is null ) return;
_previewHeightmap = heightmap;
// GPU/scene updates must happen back on the main thread
UpdatePreviewTerrain( heightmap );
}
finally
{
_isGenerating = false;
// If more changes came in while we were busy, regenerate again
if ( _previewDirty && token == _generationToken )
{
_previewDirty = false;
RegeneratePreviewAsync();
}
}
}
void OnPreFrame()
{
GizmoInstance.Input.IsHovered = IsActiveWindow && RenderCanvas.IsUnderMouse;
var isAltHeld = Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt );
var isLeftDown = Editor.Application.MouseButtons.HasFlag( MouseButtons.Left );
var isInteracting = false;
if ( GizmoInstance.OrbitCamera( Camera, RenderCanvas, ref _orbitDistance ) )
{
// User is manually orbiting - don't auto-spin this frame
isInteracting = true;
GizmoInstance.Input.IsHovered = false;
}
else if ( isAltHeld )
{
isInteracting = true;
}
else if ( isLeftDown && GizmoInstance.Input.IsHovered )
{
// Click and drag in the preview to adjust pitch/yaw
isInteracting = true;
var delta = Editor.Application.CursorDelta * 0.1f;
_orbitPitch = Math.Clamp( _orbitPitch + delta.y, 5f, 85f );
_orbitAngle += delta.x;
if ( _orbitAngle >= 360f ) _orbitAngle -= 360f;
if ( _orbitAngle < 0f ) _orbitAngle += 360f;
PositionCameraForOrbit();
}
if ( !isInteracting && _autoSpin )
{
// Slowly rotate the camera around the terrain
_orbitAngle += SpinSpeed * RealTime.Delta;
if ( _orbitAngle >= 360f ) _orbitAngle -= 360f;
PositionCameraForOrbit();
}
// Scroll wheel over the preview controls zoom
if ( !isAltHeld && GizmoInstance.Input.IsHovered && MathF.Abs( Editor.Application.MouseWheelDelta.y ) > 0.001f )
{
var wheelDelta = Editor.Application.MouseWheelDelta.y;
ZoomSlider.Value = Math.Clamp( ZoomSlider.Value + wheelDelta * 500f, ZoomSlider.Minimum, ZoomSlider.Maximum );
UpdateOrbitFromZoom();
}
RenderCanvas.UpdateGizmoInputs( GizmoInstance.Input.IsHovered );
}
void PositionCameraForOrbit()
{
if ( Camera is null || !Camera.IsValid() ) return;
float pitch = _orbitPitch;
float yaw = _orbitAngle;
var offset = new Vector3(
MathF.Sin( MathX.DegreeToRadian( yaw ) ) * MathF.Cos( MathX.DegreeToRadian( pitch ) ),
MathF.Cos( MathX.DegreeToRadian( yaw ) ) * MathF.Cos( MathX.DegreeToRadian( pitch ) ),
MathF.Sin( MathX.DegreeToRadian( pitch ) )
) * _orbitDistance;
Camera.WorldPosition = Vector3.Zero + offset;
Camera.WorldRotation = Rotation.LookAt( (Vector3.Zero - offset).Normal, Vector3.Up );
}
void UpdateOrbitFromZoom()
{
// Higher slider value = closer to terrain (zoom in)
_orbitDistance = ZoomSlider.Maximum + ZoomSlider.Minimum - ZoomSlider.Value;
PositionCameraForOrbit();
}
void BuildSplatColors()
{
// The colors must be sampled at the ACTUAL threshold positions the splatmap uses, not at
// evenly spaced positions. In Natural dispersion the layers sit at slope-weighted stops,
// so even sampling would skip/misalign colors. The gradient's frame times ARE the
// thresholds (ResampleSplatGradient positions them there), so read them directly.
int layerCount = MaxTileSplatLayers();
var frames = SplatMapGradient.Colors;
if ( frames != null && frames.Count() == layerCount && layerCount > 0 )
{
// Frames are ordered by time - use their exact positions and colors so the splatmap
// and shader reflect exactly what the user set in the gradient widget.
var times = new float[layerCount];
var colors = new SKColor[layerCount];
int i = 0;
foreach ( var frame in frames )
{
times[i] = Math.Clamp( frame.Time, 0f, 1f );
var c = frame.Value.ToColor32();
colors[i] = new SKColor( c.r, c.g, c.b, c.a );
i++;
}
_splatthresholds = times;
_splatcolors = colors;
return;
}
// Fallback: no matching frame count yet (e.g. first build) - sample the gradient at the
// same threshold positions the splatmap will use.
float[] stops;
var source = _previewHeightmap ?? _heightmap;
if ( SplatDispersion == SplatDispersionMode.Natural && source != null )
stops = ComputeNaturalThresholds( source, layerCount );
else
stops = MakeEvenThresholds( layerCount );
var thresholdtime = new List<float>();
var mapgradients = new List<SKColor>();
for ( int i = 0; i < layerCount; i++ )
{
thresholdtime.Add( stops[i] );
var color = SplatMapGradient.Evaluate( Math.Clamp( stops[i], 0f, 1f ) ).ToColor32();
mapgradients.Add( new SKColor( color.r, color.g, color.b, color.a ) );
}
_splatthresholds = thresholdtime.ToArray();
_splatcolors = mapgradients.ToArray();
}
void RegeneratePreview()
{
if ( _previewTerrain is null || !_previewTerrain.IsValid() ) return;
if ( string.IsNullOrEmpty( CategoryArray?.Selected ) || string.IsNullOrEmpty( ShapeArray?.Selected ) ) return;
BuildSplatColors();
var heightmap = BuildHeightmap(
PreviewResolution, PreviewResolution,
(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(), TerrainGridSize,
(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),
(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),
(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),
(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),
RiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,
RiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,
StagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );
if ( heightmap is null ) return;
_previewHeightmap = heightmap;
UpdatePreviewTerrain( heightmap );
}
void UpdatePreviewTerrain( float[,] heightmap )
{
if ( _previewTerrain is null || !_previewTerrain.IsValid() ) return;
if ( _previewStorage is null ) return;
int res = heightmap.GetLength( 0 );
// Resize the storage to match the incoming heightmap so Generate (full res) and the
// live preview (PreviewResolution) both work without a buffer size mismatch.
if ( _previewStorage.Resolution != res )
_previewStorage.SetResolution( res );
// Write the heightmap into the terrain storage (0..65535 maps across TerrainHeight)
ushort[] heightArray = new ushort[res * res];
for ( int y = 0; y < res; y++ )
{
for ( int x = 0; x < res; x++ )
{
float h = Math.Clamp( heightmap[x, y], 0f, 1f );
heightArray[y * res + x] = (ushort)Math.Clamp( (int)(h * 65535f), 0, 65535 );
}
}
_previewStorage.HeightMap = heightArray;
// Build the control map (which materials go where). When the material preview is
// enabled and we have assigned bluedock materials, blend them by the splat map
// exactly like the exported terrain would. Otherwise use the single default material.
uint[] controlMap = new uint[res * res];
bool useMaterials = _activePropsTab == "Splat" && PreviewSplatMaterials && _previewMaterials != null && _previewMaterials.Length > 0;
if ( useMaterials )
{
float[,] splatmap = BuildTileGridSplatmap( heightmap, TerrainGridSize,
(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );
int matCount = _previewMaterials.Length;
for ( int y = 0; y < res; y++ )
{
for ( int x = 0; x < res; x++ )
{
float layerPos = Math.Clamp( splatmap[x, y], 0f, matCount - 1f );
int baseId = (int)MathF.Floor( layerPos );
int overlayId = Math.Min( baseId + 1, matCount - 1 );
byte blend = (byte)Math.Clamp( (int)((layerPos - baseId) * 255f), 0, 255 );
controlMap[y * res + x] = new CompactTerrainMaterial( (byte)baseId, (byte)overlayId, blend, false ).Packed;
}
}
}
else
{
// Default: single material, no blending
for ( int i = 0; i < controlMap.Length; i++ )
{
controlMap[i] = new CompactTerrainMaterial( 0, 0, 0, false ).Packed;
}
}
_previewStorage.ControlMap = controlMap;
// Assign the materials and push everything to the GPU
if ( _previewMaterials != null )
{
_previewStorage.Materials.Clear();
_previewStorage.Materials.AddRange( _previewMaterials );
}
// Only the terrain shows when we're on the Splat tab previewing the real materials.
// The terrain must be enabled before touching its GPU state, otherwise SyncGPUTexture
// throws - so sync only when it's going to be visible.
if ( _previewTerrain != null ) _previewTerrain.Enabled = useMaterials;
if ( useMaterials && _previewTerrain != null && _previewTerrain.IsValid() )
{
_previewTerrain.Create();
_previewTerrain.SyncGPUTexture();
_previewTerrain.UpdateMaterialsBuffer();
}
// Overlay logic: on the Splat tab with the material preview off, overlay the splatmap
// colors so you can see the layer layout. On any other tab, overlay the original
// height-based color we used to paint the mesh. When materials are previewing, no overlay.
bool showSplatOverlay = _activePropsTab == "Splat" && !useMaterials;
UpdateSplatOverlay( heightmap, !useMaterials, showSplatOverlay );
}
void UpdateSplatOverlay( float[,] heightmap, bool visible, bool splatColors )
{
if ( _splatOverlayRenderer is null || !_splatOverlayRenderer.IsValid() ) return;
int res = heightmap.GetLength( 0 );
// Store the target heightmap and the desired colors. The mesh itself is animated
// toward this target in FrameUpdate so changes morph smoothly instead of snapping.
if ( _overlayTargetHeights == null || _overlayTargetHeights.Length != res * res )
{
_overlayTargetHeights = new float[res * res];
_overlayCurrentHeights = new float[res * res];
}
bool first = _splatOverlayRenderer.Model is null;
for ( int y = 0; y < res; y++ )
{
for ( int x = 0; x < res; x++ )
{
_overlayTargetHeights[y * res + x] = Math.Clamp( heightmap[x, y], 0f, 1f );
}
}
// If we've never built the mesh, snap to the current values so the first frame is correct.
if ( first )
{
Array.Copy( _overlayTargetHeights, _overlayCurrentHeights, _overlayTargetHeights.Length );
_overlayAnimating = false;
}
else
{
_overlayAnimating = true;
}
// Cache the splatmap (only changes when the heightmap regenerates). The vertex colors
// are evaluated from the LIVE gradient each frame so they stay in sync with the widget.
_overlayUseSplatColors = splatColors;
if ( splatColors )
_overlaySplatmap = BuildTileGridSplatmap( heightmap, TerrainGridSize,
(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );
if ( first )
BuildOverlayMesh();
// Toggle visibility
if ( _splatOverlayGO != null ) _splatOverlayGO.Enabled = visible;
}
/// <summary>
/// Called every frame - eases the overlay mesh from its current shape toward the target shape.
/// </summary>
void UpdateOverlayAnimation()
{
if ( _splatOverlayRenderer is null || !_splatOverlayRenderer.IsValid() ) return;
if ( !_overlayAnimating || _overlayTargetHeights is null || _overlayCurrentHeights is null ) return;
int count = _overlayTargetHeights.Length;
if ( _overlayCurrentHeights.Length != count ) return;
// Exponential approach - fast at first, settles smoothly
float t = 1f - MathF.Exp( -PreviewMorphSpeed * RealTime.Delta );
float maxDelta = 0f;
for ( int i = 0; i < count; i++ )
{
float delta = _overlayTargetHeights[i] - _overlayCurrentHeights[i];
_overlayCurrentHeights[i] += delta * t;
maxDelta = MathF.Max( maxDelta, MathF.Abs( delta ) );
}
// Rebuild the mesh from the current (eased) heights. Colors are sampled from the
// live gradient inside BuildOverlayMesh so they animate as smoothly as the widget.
BuildOverlayMesh();
// Stop once we're close enough
if ( maxDelta < 0.001f )
{
Array.Copy( _overlayTargetHeights, _overlayCurrentHeights, count );
_overlayAnimating = false;
}
}
void BuildOverlayMesh()
{
if ( _splatOverlayRenderer is null || !_splatOverlayRenderer.IsValid() ) return;
if ( _overlayCurrentHeights is null ) return;
int res = (int)MathF.Sqrt( _overlayCurrentHeights.Length );
int vertexCount = res * res;
const float worldSize = PreviewTerrainSize;
const float worldHeight = PreviewTerrainHeight;
float cellX = worldSize / res;
float cellY = worldSize / res;
// Color ease factor - the mesh morphs at half the gradient speed so the color
// swipe across the terrain is smoother. First build snaps immediately.
// Tile-selection grey/blue changes use a much faster rate so they snap quicker.
float colorMorphSpeed = _overlayColorAnimating ? PreviewMorphSpeed * 4f : MeshMorphSpeed;
float colorT = _overlayCurrentColors is null ? 1f : 1f - MathF.Exp( -colorMorphSpeed * RealTime.Delta );
var vertices = new Vertex[vertexCount];
var colors = _overlayCurrentColors ?? new Color32[vertexCount];
bool useSplat = _overlayUseSplatColors && _overlaySplatmap != null;
// When a specific tile is selected, only that tile keeps its colors - the rest go grey
int grid = Math.Max( TerrainGridSize, 1 );
int tileW = res / grid;
int tileH = res / grid;
// Track how far the colors moved so the refresh can stop once they settle
float[] maxColorDelta = new float[1];
Parallel.For( 0, res, y =>
{
for ( int x = 0; x < res; x++ )
{
int index = y * res + x;
float h = _overlayCurrentHeights[index];
// Local space - the overlay GO is parented to the centered terrain GO
Vector3 position = new Vector3( x * cellX, y * cellY, h * worldHeight );
float hL = _overlayCurrentHeights[y * res + Math.Max( x - 1, 0 )];
float hR = _overlayCurrentHeights[y * res + Math.Min( x + 1, res - 1 )];
float hD = _overlayCurrentHeights[Math.Max( y - 1, 0 ) * res + x];
float hU = _overlayCurrentHeights[Math.Min( y + 1, res - 1 ) * res + x];
float dx = (hR - hL) * worldHeight / (2.0f * cellX);
float dy = (hU - hD) * worldHeight / (2.0f * cellY);
Vector3 normal = new Vector3( -dx, -dy, 1.0f ).Normal;
// Which grid tile does this vertex belong to?
int tileX = Math.Min( x / Math.Max( tileW, 1 ), grid - 1 );
int tileY = Math.Min( y / Math.Max( tileH, 1 ), grid - 1 );
int tileIndex = tileY * grid + tileX;
bool isSelectedTile = _selectedTileIndex < 0 || tileIndex == _selectedTileIndex;
// Sample the target color from the LIVE gradient each frame, then ease the
// mesh color toward it so the swipe lags behind the widget and looks smooth.
Color targetColor;
if ( !isSelectedTile )
{
// Wash out everything outside the selected tile
targetColor = Color.FromBytes( 235, 235, 235 );
}
else if ( useSplat )
{
int tileLayers = _tileSplatLayerCounts != null && tileIndex < _tileSplatLayerCounts.Length
? Math.Max( _tileSplatLayerCounts[tileIndex], 2 ) : Math.Max( SplatLayerCount, 2 );
// Sample the color from the same threshold-aligned color table the splatmap
// image uses, so the 3D preview and the exported splatmap always agree.
float layerPos = Math.Clamp( _overlaySplatmap[x, y], 0f, tileLayers - 1f );
int layer0 = (int)MathF.Floor( layerPos );
int layer1 = Math.Min( layer0 + 1, tileLayers - 1 );
float t = layerPos - layer0;
if ( _splatcolors != null && _splatcolors.Length > layer1 )
{
var col0 = _splatcolors[layer0];
var col1 = _splatcolors[layer1];
targetColor = new Color(
MathX.LerpTo( col0.Red / 255f, col1.Red / 255f, t ),
MathX.LerpTo( col0.Green / 255f, col1.Green / 255f, t ),
MathX.LerpTo( col0.Blue / 255f, col1.Blue / 255f, t ) );
}
else
{
Color c0 = SplatMapGradient.Evaluate( Math.Clamp( layer0 / (float)Math.Max( tileLayers - 1, 1 ), 0f, 1f ) );
Color c1 = SplatMapGradient.Evaluate( Math.Clamp( layer1 / (float)Math.Max( tileLayers - 1, 1 ), 0f, 1f ) );
targetColor = Color.Lerp( c0, c1, t );
}
}
else
{
// The original height-based material color we painted on the mesh
targetColor = Color.Lerp( Color.FromBytes( 30, 90, 200 ), Color.FromBytes( 200, 185, 150 ), h );
}
var target32 = targetColor.ToColor32();
var current = colors[index];
byte r = (byte)MathX.LerpTo( current.r, target32.r, colorT );
byte g = (byte)MathX.LerpTo( current.g, target32.g, colorT );
byte b = (byte)MathX.LerpTo( current.b, target32.b, colorT );
byte a = (byte)MathX.LerpTo( current.a, target32.a, colorT );
colors[index] = new Color32( r, g, b, a );
float delta = MathF.Abs( r - target32.r ) + MathF.Abs( g - target32.g ) + MathF.Abs( b - target32.b );
maxColorDelta[0] = MathF.Max( maxColorDelta[0], delta );
vertices[index] = new Vertex( position, normal, normal, new Vector4( 0, 0, 0, 1 ) );
vertices[index].Color = colors[index];
}
} );
_overlayCurrentColors = colors;
// Stop the color-only refresh once everything has eased to its target
if ( _overlayColorAnimating && maxColorDelta[0] < 1f )
{
_overlayColorAnimating = false;
}
// Build indices once - the grid topology never changes
var indices = new List<int>();
if ( _overlayMesh is null )
{
for ( int y = 0; y < res - 1; y++ )
{
for ( int x = 0; x < res - 1; x++ )
{
int a = x + y * res;
int b = (x + 1) + y * res;
int c = (x + 1) + (y + 1) * res;
int d = x + (y + 1) * res;
indices.Add( a );
indices.Add( b );
indices.Add( c );
indices.Add( a );
indices.Add( c );
indices.Add( d );
}
}
}
if ( _overlayMesh is null || !_overlayMesh.IsValid() )
{
_overlayMesh = new Mesh( _splatOverlayRenderer.MaterialOverride );
_overlayMesh.CreateVertexBuffer( vertices.Length, vertices );
_overlayMesh.CreateIndexBuffer( indices.Count, indices );
_overlayMesh.Bounds = BBox.FromPositionAndSize( new Vector3( worldSize * 0.5f, worldSize * 0.5f, worldHeight * 0.5f ), new Vector3( worldSize, worldSize, worldHeight ) );
_splatOverlayRenderer.Model = Model.Builder.AddMesh( _overlayMesh ).Create();
}
else
{
// Update the existing vertex buffer in place - much faster than rebuilding the model
_overlayMesh.SetVertexBufferData( vertices );
}
}
float[,] BuildHeightmap( int width, int height,
string[] tileCategories,
string[] tileShapes,
int gridSize,
long[] tileSeeds,
int[] tileNoiseLayerStacks,
float[] tileMinHeights,
float[] tileMaxHeights,
bool[] tileDomainWarping,
float[] tileDomainWarpingSizes,
float[] tileDomainWarpingStrengths,
int[] tileSmoothingPasses,
float[] tilePlaneScales,
bool riverCarving,
float riverFrequency,
float riverWidth,
float riverDepth,
float riverTurbulenceFrequency,
float riverTurbulenceStrength,
float minRiverSpacing,
bool stagingArea,
int stagingAreaSize,
float stagingAreaHeight,
float stagingAreaX,
float stagingAreaY )
{
int grid = Math.Max( gridSize, 1 );
int tileW = width / grid;
int tileH = height / grid;
// Build each tile's heightmap, then stitch them together averaging overlapping edges.
float[,] heightmap = BuildTileGrid( width, height, tileW, tileH, grid, tileCategories, tileShapes, tileSeeds, tileNoiseLayerStacks, tileMinHeights, tileMaxHeights, tileDomainWarping, tileDomainWarpingSizes, tileDomainWarpingStrengths, tileSmoothingPasses, tilePlaneScales );
if ( ErosionSimulation )
{
}
if ( heightmap == null )
{
Log.Error( "Heightmap is not generated. Aborting." );
return null;
}
// Rivers are carved across the whole combined map so they flow continuously through the tiles
if ( riverCarving )
{
heightmap = AddTurbulenceForRivers(
heightmap,
seed: tileSeeds != null && tileSeeds.Length > 0 ? tileSeeds[0] : 0,
riverFrequency: riverFrequency,
riverWidth: riverWidth,
riverDepth: riverDepth,
turbulenceFrequency: riverTurbulenceFrequency,
turbulenceStrength: riverTurbulenceStrength,
minRiverSpacing: minRiverSpacing,
slopeSteepness:10f,
terrainNoiseFrequency: 2.0f,
terrainNoiseAmplitude: 0.5f
);
}
if ( stagingArea )
{
heightmap = AddStagingSquare(
heightmap,
stagingAreaSize,
stagingAreaHeight,
stagingAreaX,
stagingAreaY );
}
return heightmap;
}
/// <summary>
/// Generates each tile with its own category/shape/height/scale/seed, then stitches them into
/// one heightmap. Adjacent tiles share a blend band so their edges are averaged and look continuous.
/// </summary>
float[,] BuildTileGrid( int width, int height, int tileW, int tileH, int grid, string[] categories, string[] shapes, long[] seeds, int[] noiseLayersArr, float[] minHeights, float[] maxHeights, bool[] warpingArr, float[] warpingSizesArr, float[] warpingStrengthsArr, int[] smoothingArr, float[] planeScales )
{
float[,] result = new float[width, height];
// Single tile - just generate it directly at the requested size, matching the old behavior exactly.
if ( grid <= 1 )
{
string category = categories != null && categories.Length > 0 ? categories[0] : null;
string shape = shapes != null && shapes.Length > 0 ? shapes[0] : null;
long seed = seeds != null && seeds.Length > 0 ? seeds[0] : 0;
int layerCount = noiseLayersArr != null && noiseLayersArr.Length > 0 ? noiseLayersArr[0] : 1;
float minHeight = minHeights != null && minHeights.Length > 0 ? minHeights[0] : 0.2f;
float maxHeight = maxHeights != null && maxHeights.Length > 0 ? maxHeights[0] : 0.5f;
int smoothingPasses = smoothingArr != null && smoothingArr.Length > 0 ? smoothingArr[0] : 0;
float planeScale = planeScales != null && planeScales.Length > 0 ? planeScales[0] : 0.5f;
bool domainWarping = warpingArr != null && warpingArr.Length > 0 ? warpingArr[0] : true;
float domainWarpingSize = warpingSizesArr != null && warpingSizesArr.Length > 0 ? warpingSizesArr[0] : 0.25f;
float domainWarpingStrength = warpingStrengthsArr != null && warpingStrengthsArr.Length > 0 ? warpingStrengthsArr[0] : 0.15f;
if ( string.IsNullOrEmpty( category ) ) category = "Islands";
if ( string.IsNullOrEmpty( shape ) ) shape = "Default";
var fullclass = Type.GetType( $"Sturnus.TerrainGenerationTool.{category}" );
if ( fullclass is null || fullclass.GetMethod( shape ) is null ) return null;
return GenerateStackedNoise(
width, height,
seed,
layerCount,
1.0f, 2.0f, 1.0f, 0.5f,
( x, y ) => (float)CallMethod( $"Sturnus.TerrainGenerationTool.{category}", shape, new object[] {
x, y,
width, height,
seed,
minHeight,
domainWarping,
domainWarpingSize,
domainWarpingStrength
} ),
maxHeight,
smoothingPasses,
planeScale
);
}
float[,] weight = new float[width, height];
// Overlap width for edge blending - a fraction of the tile size so seams blend smoothly
int blend = Math.Max( 2, Math.Min( tileW, tileH ) / 8 );
for ( int ty = 0; ty < grid; ty++ )
{
for ( int tx = 0; tx < grid; tx++ )
{
int index = ty * grid + tx;
string category = categories != null && index < categories.Length && !string.IsNullOrEmpty( categories[index] ) ? categories[index] : "Islands";
string shape = shapes != null && index < shapes.Length && !string.IsNullOrEmpty( shapes[index] ) ? shapes[index] : "Default";
long tileSeed = seeds != null && index < seeds.Length ? seeds[index] : 0;
int layerCount = noiseLayersArr != null && index < noiseLayersArr.Length ? noiseLayersArr[index] : 1;
float minHeight = minHeights != null && index < minHeights.Length ? minHeights[index] : 0.2f;
float maxHeight = maxHeights != null && index < maxHeights.Length ? maxHeights[index] : 0.5f;
int smoothingPasses = smoothingArr != null && index < smoothingArr.Length ? smoothingArr[index] : 0;
float planeScale = planeScales != null && index < planeScales.Length ? planeScales[index] : 0.5f;
bool domainWarping = warpingArr != null && index < warpingArr.Length ? warpingArr[index] : true;
float domainWarpingSize = warpingSizesArr != null && index < warpingSizesArr.Length ? warpingSizesArr[index] : 0.25f;
float domainWarpingStrength = warpingStrengthsArr != null && index < warpingStrengthsArr.Length ? warpingStrengthsArr[index] : 0.15f;
var fullclass = Type.GetType( $"Sturnus.TerrainGenerationTool.{category}" );
if ( fullclass is null || fullclass.GetMethod( shape ) is null )
continue;
float[,] tile = GenerateStackedNoise(
tileW + blend * 2,
tileH + blend * 2,
tileSeed,
layerCount,
1.0f,
2.0f,
1.0f,
0.5f,
( x, y ) => (float)CallMethod( $"Sturnus.TerrainGenerationTool.{category}", shape, new object[] {
x, y,
tileW + blend * 2,
tileH + blend * 2,
tileSeed,
minHeight,
domainWarping,
domainWarpingSize,
domainWarpingStrength
} ),
maxHeight,
smoothingPasses,
planeScale
);
if ( tile is null ) continue;
// Place the tile into the result with a weighted blend on the edges.
// The tile is generated slightly larger than its slot (tileW + blend*2) so the
// overlap regions between neighbors are averaged.
int slotX = tx * tileW;
int slotY = ty * tileH;
for ( int y = 0; y < tileH + blend * 2; y++ )
{
int outY = slotY - blend + y;
if ( outY < 0 || outY >= height ) continue;
for ( int x = 0; x < tileW + blend * 2; x++ )
{
int outX = slotX - blend + x;
if ( outX < 0 || outX >= width ) continue;
// Edge weight: 1 in the core, fading to 0 across the blend band at each edge
float wx = EdgeBlendWeight( x, tileW + blend * 2, blend );
float wy = EdgeBlendWeight( y, tileH + blend * 2, blend );
float w = wx * wy;
result[outX, outY] += tile[x, y] * w;
weight[outX, outY] += w;
}
}
}
}
// Normalize by the accumulated weights
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
if ( weight[x, y] > 0.0001f )
result[x, y] /= weight[x, y];
}
}
return result;
}
/// <summary>
/// Generates each grid cell as its own full-resolution heightmap using that cell's own
/// category/shape/height/scale/seed/smoothing/noise/warp settings. No stitching - each cell
/// is a complete map of width x height. Rivers and staging are applied per cell.
/// </summary>
List<float[,]> BuildPerCellHeightmaps( int width, int height,
string[] categories, string[] shapes, long[] seeds, int[] noiseLayersArr,
float[] minHeights, float[] maxHeights, bool[] warpingArr, float[] warpingSizesArr,
float[] warpingStrengthsArr, int[] smoothingArr, float[] planeScales,
bool riverCarving, float riverFrequency, float riverWidth, float riverDepth,
float riverTurbulenceFrequency, float riverTurbulenceStrength, float minRiverSpacing,
bool stagingArea, int stagingAreaSize, float stagingAreaHeight, float stagingAreaX, float stagingAreaY )
{
var cells = new List<float[,]>();
int grid = Math.Max( TerrainGridSize, 1 );
int count = grid * grid;
for ( int index = 0; index < count; index++ )
{
string category = categories != null && index < categories.Length && !string.IsNullOrEmpty( categories[index] ) ? categories[index] : "Islands";
string shape = shapes != null && index < shapes.Length && !string.IsNullOrEmpty( shapes[index] ) ? shapes[index] : "Default";
long cellSeed = seeds != null && index < seeds.Length ? seeds[index] : 0;
int layerCount = noiseLayersArr != null && index < noiseLayersArr.Length ? noiseLayersArr[index] : 1;
float minHeight = minHeights != null && index < minHeights.Length ? minHeights[index] : 0.2f;
float maxHeight = maxHeights != null && index < maxHeights.Length ? maxHeights[index] : 0.5f;
bool domainWarping = warpingArr != null && index < warpingArr.Length ? warpingArr[index] : true;
float domainWarpingSize = warpingSizesArr != null && index < warpingSizesArr.Length ? warpingSizesArr[index] : 0.25f;
float domainWarpingStrength = warpingStrengthsArr != null && index < warpingStrengthsArr.Length ? warpingStrengthsArr[index] : 0.15f;
int smoothingPasses = smoothingArr != null && index < smoothingArr.Length ? smoothingArr[index] : 0;
float planeScale = planeScales != null && index < planeScales.Length ? planeScales[index] : 0.5f;
var fullclass = Type.GetType( $"Sturnus.TerrainGenerationTool.{category}" );
if ( fullclass is null || fullclass.GetMethod( shape ) is null )
continue;
float[,] map = GenerateStackedNoise(
width, height,
cellSeed,
layerCount,
1.0f, 2.0f, 1.0f, 0.5f,
( x, y ) => (float)CallMethod( $"Sturnus.TerrainGenerationTool.{category}", shape, new object[] {
x, y,
width, height,
cellSeed,
minHeight,
domainWarping,
domainWarpingSize,
domainWarpingStrength
} ),
maxHeight,
smoothingPasses,
planeScale
);
if ( map is null ) continue;
if ( riverCarving )
{
map = AddTurbulenceForRivers( map, cellSeed, riverFrequency, riverWidth, riverDepth,
riverTurbulenceFrequency, riverTurbulenceStrength, minRiverSpacing, 10f, 2.0f, 0.5f );
}
if ( stagingArea )
{
map = AddStagingSquare( map, stagingAreaSize, stagingAreaHeight, stagingAreaX, stagingAreaY );
}
cells.Add( map );
}
return cells;
}
/// <summary>
/// Generates the splatmap cache for per-cell mode - one full-res splatmap per cell using that
/// cell's own layer count / dispersion / blend strength.
/// </summary>
List<float[,]> BuildPerCellSplatmaps( List<float[,]> cellHeightmaps, int[] layerCounts, SplatDispersionMode[] dispersions, float[] blendStrengths )
{
var splats = new List<float[,]>();
if ( cellHeightmaps is null ) return splats;
for ( int i = 0; i < cellHeightmaps.Count; i++ )
{
int layers = layerCounts != null && i < layerCounts.Length ? Math.Max( layerCounts[i], 2 ) : 2;
var dispersion = dispersions != null && i < dispersions.Length ? dispersions[i] : SplatDispersionMode.Evenly;
float blend = blendStrengths != null && i < blendStrengths.Length ? blendStrengths[i] : 0.35f;
splats.Add( GenerateSplatmap( cellHeightmaps[i], MakeEvenThresholds( layers ), TerrainMaxHeight, layers, dispersion, blend ) );
}
return splats;
}
/// <summary>
/// Generates a splatmap where each grid tile uses its own layer count, dispersion mode and
/// blend strength. Each tile's splatmap is computed over its padded region (with the blend
/// overlap) and stitched together using the same edge weights as the heightmap.
/// </summary>
float[,] BuildTileGridSplatmap( float[,] heightmap, int gridSize, int[] layerCounts, SplatDispersionMode[] dispersions, float[] blendStrengths )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
int grid = Math.Max( gridSize, 1 );
int tileW = width / grid;
int tileH = height / grid;
int maxLayers = 2;
if ( layerCounts != null )
{
foreach ( var lc in layerCounts )
maxLayers = Math.Max( maxLayers, lc );
}
// Single tile - same as before, just uses the tile's settings.
if ( grid <= 1 )
{
int layers = layerCounts != null && layerCounts.Length > 0 ? Math.Max( layerCounts[0], 2 ) : maxLayers;
var dispersion = dispersions != null && dispersions.Length > 0 ? dispersions[0] : SplatDispersionMode.Evenly;
float blendStrength = blendStrengths != null && blendStrengths.Length > 0 ? blendStrengths[0] : 0.35f;
return GenerateSplatmap( heightmap, MakeEvenThresholds( layers ), TerrainMaxHeight, layers, dispersion, blendStrength );
}
float[,] result = new float[width, height];
float[,] weight = new float[width, height];
int blend = Math.Max( 2, Math.Min( tileW, tileH ) / 8 );
for ( int ty = 0; ty < grid; ty++ )
{
for ( int tx = 0; tx < grid; tx++ )
{
int index = ty * grid + tx;
int layers = layerCounts != null && index < layerCounts.Length ? Math.Max( layerCounts[index], 2 ) : maxLayers;
var dispersion = dispersions != null && index < dispersions.Length ? dispersions[index] : SplatDispersionMode.Evenly;
float blendStrength = blendStrengths != null && index < blendStrengths.Length ? blendStrengths[index] : 0.35f;
// Extract the tile's heightmap region (with blend padding) so its splatmap
// normalizes against the tile's own height range.
int tw = tileW + blend * 2;
int th = tileH + blend * 2;
float[,] tileHeight = new float[tw, th];
int slotX = tx * tileW;
int slotY = ty * tileH;
for ( int y = 0; y < th; y++ )
{
int srcY = slotY - blend + y;
if ( srcY < 0 || srcY >= height ) continue;
for ( int x = 0; x < tw; x++ )
{
int srcX = slotX - blend + x;
if ( srcX < 0 || srcX >= width ) continue;
tileHeight[x, y] = heightmap[srcX, srcY];
}
}
float[,] tileSplat = GenerateSplatmap( tileHeight, MakeEvenThresholds( layers ), TerrainMaxHeight, layers, dispersion, blendStrength );
// Stitch with edge weights - same as the heightmap tiles
for ( int y = 0; y < th; y++ )
{
int outY = slotY - blend + y;
if ( outY < 0 || outY >= height ) continue;
for ( int x = 0; x < tw; x++ )
{
int outX = slotX - blend + x;
if ( outX < 0 || outX >= width ) continue;
float wx = EdgeBlendWeight( x, tw, blend );
float wy = EdgeBlendWeight( y, th, blend );
float w = wx * wy;
result[outX, outY] += tileSplat[x, y] * w;
weight[outX, outY] += w;
}
}
}
}
// Normalize by the accumulated weights
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
if ( weight[x, y] > 0.0001f )
result[x, y] /= weight[x, y];
}
}
return result;
}
/// <summary>
/// Builds evenly spaced color stop positions for the given layer count.
/// </summary>
float[] MakeEvenThresholds( int layers )
{
var t = new float[layers];
for ( int i = 0; i < layers; i++ )
{
t[i] = layers <= 1 ? 0f : (float)i / (layers - 1);
}
return t;
}
float EdgeBlendWeight( int coord, int size, int blend )
{
if ( blend <= 0 ) return 1f;
if ( coord < blend ) return (float)coord / blend;
if ( coord > size - blend ) return (float)(size - coord) / blend;
return 1f;
}
public void RebuildShapes()
{
ShapeArray.DestroyChildren();
TerrainShapeArray.Clear();
if ( CategoryArray?.Selected is null )
{
return;
}
string className = $"Sturnus.TerrainGenerationTool.{TerrainCategoryEnum.GetName( TerrainCategoryEnum.GetValue( CategoryArray.Selected ) )}"; // Fully qualified name
string[] methods = GetMethodsFromClass( className );
// Print the methods
foreach ( string method in methods )
{
TerrainShapeArray.Add( method );
//Log.Info( method );
}
foreach ( var shape in TerrainShapeArray )
{
ShapeArray.AddOption( shape );
}
ShapeArray.SelectedIndex = 0;
ShapeArray.Selected = ShapeArray.Children.FirstOrDefault().Name;
foreach(var test in ShapeArray.Children )
{
//Log.Info( test.Name );
}
}
/// <summary>
/// Rebuilds the tile grid section: an "All" box plus one box per grid cell. Each box shows
/// which terrain category that tile currently uses and is clickable to select which tile the
/// Terrain Type page's category/shape selectors edit.
/// </summary>
void RebuildTileGridUI()
{
if ( _tilesContainer is null || !_tilesContainer.IsValid() ) return;
_tilesContainer.DestroyChildren();
_tileBoxes.Clear();
int grid = Math.Max( TerrainGridSize, 1 );
int count = grid * grid;
// Preserve existing selections, extend/trim to the new size
var oldCategories = _tileCategories;
var oldShapes = _tileShapes;
var oldMinHeights = _tileMinHeights;
var oldMaxHeights = _tileMaxHeights;
var oldPlaneScales = _tilePlaneScales;
var oldSeeds = _tileSeeds;
var oldSmoothing = _tileSmoothingPasses;
var oldNoiseLayers = _tileNoiseLayerStacks;
var oldWarping = _tileDomainWarping;
var oldWarpingSizes = _tileDomainWarpingSizes;
var oldWarpingStrengths = _tileDomainWarpingStrengths;
var oldSplatLayerCounts = _tileSplatLayerCounts;
var oldSplatMapCounts = _tileSplatMapCounts;
var oldSplatDispersions = _tileSplatDispersions;
var oldSplatBlendStrengths = _tileSplatBlendStrengths;
_tileCategories = new string[count];
_tileShapes = new string[count];
_tileMinHeights = new float[count];
_tileMaxHeights = new float[count];
_tilePlaneScales = new float[count];
_tileSeeds = new long[count];
_tileSmoothingPasses = new int[count];
_tileNoiseLayerStacks = new int[count];
_tileDomainWarping = new bool[count];
_tileDomainWarpingSizes = new float[count];
_tileDomainWarpingStrengths = new float[count];
_tileSplatLayerCounts = new int[count];
_tileSplatMapCounts = new int[count];
_tileSplatDispersions = new SplatDispersionMode[count];
_tileSplatBlendStrengths = new float[count];
string defaultCategory = TerrainCategoryArray.Count > 0 ? TerrainCategoryArray.First() : CategoryArray?.Selected;
string defaultShape = FirstShapeForCategory( defaultCategory );
if ( string.IsNullOrEmpty( defaultShape ) )
defaultShape = ShapeArray?.Selected;
for ( int i = 0; i < count; i++ )
{
_tileCategories[i] = oldCategories != null && i < oldCategories.Length && !string.IsNullOrEmpty( oldCategories[i] )
? oldCategories[i] : defaultCategory;
_tileShapes[i] = oldShapes != null && i < oldShapes.Length && !string.IsNullOrEmpty( oldShapes[i] )
? oldShapes[i] : defaultShape;
_tileMinHeights[i] = oldMinHeights != null && i < oldMinHeights.Length ? oldMinHeights[i] : TerrainMinHeight;
_tileMaxHeights[i] = oldMaxHeights != null && i < oldMaxHeights.Length ? oldMaxHeights[i] : TerrainMaxHeight;
_tilePlaneScales[i] = oldPlaneScales != null && i < oldPlaneScales.Length ? oldPlaneScales[i] : TerrainPlaneScale;
_tileSeeds[i] = oldSeeds != null && i < oldSeeds.Length ? oldSeeds[i] : TerrainSeed;
_tileSmoothingPasses[i] = oldSmoothing != null && i < oldSmoothing.Length ? oldSmoothing[i] : SmoothingPasses;
_tileNoiseLayerStacks[i] = oldNoiseLayers != null && i < oldNoiseLayers.Length ? oldNoiseLayers[i] : NoiseLayerStacks;
_tileDomainWarping[i] = oldWarping != null && i < oldWarping.Length ? oldWarping[i] : DomainWarping;
_tileDomainWarpingSizes[i] = oldWarpingSizes != null && i < oldWarpingSizes.Length ? oldWarpingSizes[i] : DomainWarpingSize;
_tileDomainWarpingStrengths[i] = oldWarpingStrengths != null && i < oldWarpingStrengths.Length ? oldWarpingStrengths[i] : DomainWarpingStrength;
_tileSplatLayerCounts[i] = oldSplatLayerCounts != null && i < oldSplatLayerCounts.Length ? oldSplatLayerCounts[i] : SplatLayerCount;
_tileSplatMapCounts[i] = oldSplatMapCounts != null && i < oldSplatMapCounts.Length ? oldSplatMapCounts[i] : SplatMapCount;
_tileSplatDispersions[i] = oldSplatDispersions != null && i < oldSplatDispersions.Length ? oldSplatDispersions[i] : SplatDispersion;
_tileSplatBlendStrengths[i] = oldSplatBlendStrengths != null && i < oldSplatBlendStrengths.Length ? oldSplatBlendStrengths[i] : SplatBlendStrength;
}
_selectedTileIndex = Math.Clamp( _selectedTileIndex, 0, count - 1 );
// "All tiles" box selects every tile at once
var allBox = new TileGridBox( null, -1, "All", "Select every tile" );
allBox.IsSelected = _selectedTileIndex < 0;
allBox.OnClicked = () => SelectTile( -1 );
_tileBoxes.Add( allBox );
_tilesContainer.Layout.Add( allBox );
// One box per grid cell
for ( int ty = 0; ty < grid; ty++ )
{
var row = _tilesContainer.Layout.AddRow();
row.Spacing = 4;
for ( int tx = 0; tx < grid; tx++ )
{
int index = ty * grid + tx;
var box = new TileGridBox( null, index, $"{_tileCategories[index]}:{_tileShapes[index]}", $"Tile {tx},{ty}" );
box.IsSelected = index == _selectedTileIndex;
box.OnClicked = () => SelectTile( index );
_tileBoxes.Add( box );
row.Add( box, 1 );
}
}
// Sync the category/shape selectors to whichever tile is selected
SyncSelectorsToSelectedTile();
}
/// <summary>
/// Marks the given tile as the one being edited. -1 selects all tiles.
/// </summary>
void SelectTile( int index )
{
if ( _selectedTileIndex == index ) return;
_selectedTileIndex = index;
UpdateTileBoxSelection();
SyncSelectorsToSelectedTile();
// Ease the overlay mesh colors so only the selected tile stays colored
if ( _overlayMesh != null && _overlayMesh.IsValid() )
{
_overlayColorAnimating = true;
}
}
void UpdateTileBoxSelection()
{
foreach ( var box in _tileBoxes )
{
if ( box.Index == _selectedTileIndex )
{
box.IsSelected = true;
box.Update();
}
else
{
box.IsSelected = false;
box.Update();
}
}
}
void UpdateTileBoxText()
{
foreach ( var box in _tileBoxes )
{
if ( box.Index < 0 ) continue;
if ( box.Index < _tileCategories.Length && box.Index < _tileShapes.Length )
box.Text = $"{_tileCategories[box.Index]}:{_tileShapes[box.Index]}";
box.Update();
}
}
/// <summary>
/// Pushes the selected tile's category/shape/height/scale/seed into the Terrain Type and
/// Height/Scale page controls.
/// </summary>
void SyncSelectorsToSelectedTile()
{
if ( CategoryArray is null || ShapeArray is null ) return;
_syncingTileSelectors = true;
int refIndex = Math.Max( _selectedTileIndex, 0 );
if ( refIndex >= _tileCategories.Length ) refIndex = 0;
string category = _tileCategories[refIndex];
if ( CategoryArray.HasOption( category ) )
{
CategoryArray.Selected = category;
}
else if ( CategoryArray.Children.Count() > 0 )
{
CategoryArray.SelectedIndex = 0;
}
string shape = _tileShapes[refIndex];
if ( ShapeArray.HasOption( shape ) )
{
ShapeArray.Selected = shape;
}
else if ( ShapeArray.Children.Count() > 0 )
{
ShapeArray.SelectedIndex = 0;
}
// Push the selected tile's height/scale/seed into the global props so the
// Height/Scale page sliders show the selected tile's values.
_serialized.GetProperty( nameof( TerrainMinHeight ) )?.SetValue( _tileMinHeights[refIndex] );
_serialized.GetProperty( nameof( TerrainMaxHeight ) )?.SetValue( _tileMaxHeights[refIndex] );
_serialized.GetProperty( nameof( TerrainPlaneScale ) )?.SetValue( _tilePlaneScales[refIndex] );
_serialized.GetProperty( nameof( TerrainSeed ) )?.SetValue( _tileSeeds[refIndex] );
// Same for the Smooth/Noise page controls
_serialized.GetProperty( nameof( SmoothingPasses ) )?.SetValue( _tileSmoothingPasses[refIndex] );
_serialized.GetProperty( nameof( NoiseLayerStacks ) )?.SetValue( _tileNoiseLayerStacks[refIndex] );
// Domain warping page controls
_serialized.GetProperty( nameof( DomainWarping ) )?.SetValue( _tileDomainWarping[refIndex] );
_serialized.GetProperty( nameof( DomainWarpingSize ) )?.SetValue( _tileDomainWarpingSizes[refIndex] );
_serialized.GetProperty( nameof( DomainWarpingStrength ) )?.SetValue( _tileDomainWarpingStrengths[refIndex] );
// Splat page controls
_serialized.GetProperty( nameof( SplatLayerCount ) )?.SetValue( _tileSplatLayerCounts[refIndex] );
_serialized.GetProperty( nameof( SplatMapCount ) )?.SetValue( _tileSplatMapCounts[refIndex] );
_serialized.GetProperty( nameof( SplatDispersion ) )?.SetValue( _tileSplatDispersions[refIndex] );
_serialized.GetProperty( nameof( SplatBlendStrength ) )?.SetValue( _tileSplatBlendStrengths[refIndex] );
_syncingTileSelectors = false;
}
/// <summary>
/// Called when the Terrain Type page's category changes. Writes the new category to the
/// selected tile (or every tile if All is selected).
/// </summary>
void ApplySelectedCategory()
{
if ( _syncingTileSelectors ) return;
string category = CategoryArray.Selected;
if ( string.IsNullOrEmpty( category ) ) return;
if ( _selectedTileIndex < 0 )
{
for ( int i = 0; i < _tileCategories.Length; i++ )
{
_tileCategories[i] = category;
_tileShapes[i] = FirstShapeForCategory( category );
}
}
else if ( _selectedTileIndex < _tileCategories.Length )
{
_tileCategories[_selectedTileIndex] = category;
_tileShapes[_selectedTileIndex] = FirstShapeForCategory( category );
}
// Keep the shape selector in sync with the new category's first shape
if ( ShapeArray.HasOption( _tileShapes[Math.Max( _selectedTileIndex, 0 )] ) )
ShapeArray.Selected = _tileShapes[Math.Max( _selectedTileIndex, 0 )];
UpdateTileBoxText();
}
/// <summary>
/// Called when the Terrain Type page's shape changes. Writes the new shape to the selected
/// tile (or every tile if All is selected).
/// </summary>
void ApplySelectedShape()
{
if ( _syncingTileSelectors ) return;
string shape = ShapeArray.Selected;
if ( string.IsNullOrEmpty( shape ) ) return;
if ( _selectedTileIndex < 0 )
{
for ( int i = 0; i < _tileShapes.Length; i++ )
_tileShapes[i] = shape;
}
else if ( _selectedTileIndex < _tileShapes.Length )
{
_tileShapes[_selectedTileIndex] = shape;
}
UpdateTileBoxText();
}
/// <summary>
/// Writes the given global height/scale/seed values into the selected tile (or every tile
/// if All is selected). Nullable params mean "leave unchanged".
/// </summary>
void WriteSelectedValues( float? minHeight = null, float? maxHeight = null, float? planeScale = null, long? seed = null, int? smoothing = null, int? noiseLayers = null,
bool? warp = null, float? warpSize = null, float? warpStrength = null,
int? splatLayers = null, int? splatMaps = null, SplatDispersionMode? splatDispersion = null, float? splatBlend = null )
{
if ( _selectedTileIndex < 0 )
{
for ( int i = 0; i < _tileMinHeights.Length; i++ )
{
if ( minHeight.HasValue ) _tileMinHeights[i] = minHeight.Value;
if ( maxHeight.HasValue ) _tileMaxHeights[i] = maxHeight.Value;
if ( planeScale.HasValue ) _tilePlaneScales[i] = planeScale.Value;
if ( seed.HasValue ) _tileSeeds[i] = seed.Value;
if ( smoothing.HasValue ) _tileSmoothingPasses[i] = smoothing.Value;
if ( noiseLayers.HasValue ) _tileNoiseLayerStacks[i] = noiseLayers.Value;
if ( warp.HasValue ) _tileDomainWarping[i] = warp.Value;
if ( warpSize.HasValue ) _tileDomainWarpingSizes[i] = warpSize.Value;
if ( warpStrength.HasValue ) _tileDomainWarpingStrengths[i] = warpStrength.Value;
if ( splatLayers.HasValue ) _tileSplatLayerCounts[i] = splatLayers.Value;
if ( splatMaps.HasValue ) _tileSplatMapCounts[i] = splatMaps.Value;
if ( splatDispersion.HasValue ) _tileSplatDispersions[i] = splatDispersion.Value;
if ( splatBlend.HasValue ) _tileSplatBlendStrengths[i] = splatBlend.Value;
}
}
else if ( _selectedTileIndex < _tileMinHeights.Length )
{
if ( minHeight.HasValue ) _tileMinHeights[_selectedTileIndex] = minHeight.Value;
if ( maxHeight.HasValue ) _tileMaxHeights[_selectedTileIndex] = maxHeight.Value;
if ( planeScale.HasValue ) _tilePlaneScales[_selectedTileIndex] = planeScale.Value;
if ( seed.HasValue ) _tileSeeds[_selectedTileIndex] = seed.Value;
if ( smoothing.HasValue ) _tileSmoothingPasses[_selectedTileIndex] = smoothing.Value;
if ( noiseLayers.HasValue ) _tileNoiseLayerStacks[_selectedTileIndex] = noiseLayers.Value;
if ( warp.HasValue ) _tileDomainWarping[_selectedTileIndex] = warp.Value;
if ( warpSize.HasValue ) _tileDomainWarpingSizes[_selectedTileIndex] = warpSize.Value;
if ( warpStrength.HasValue ) _tileDomainWarpingStrengths[_selectedTileIndex] = warpStrength.Value;
if ( splatLayers.HasValue ) _tileSplatLayerCounts[_selectedTileIndex] = splatLayers.Value;
if ( splatMaps.HasValue ) _tileSplatMapCounts[_selectedTileIndex] = splatMaps.Value;
if ( splatDispersion.HasValue ) _tileSplatDispersions[_selectedTileIndex] = splatDispersion.Value;
if ( splatBlend.HasValue ) _tileSplatBlendStrengths[_selectedTileIndex] = splatBlend.Value;
}
}
string FirstShapeForCategory( string category )
{
var options = ShapeOptionsForCategory( category );
return options.Length > 0 ? options[0] : null;
}
string[] ShapeOptionsForCategory( string category )
{
if ( string.IsNullOrEmpty( category ) ) return Array.Empty<string>();
string className = $"Sturnus.TerrainGenerationTool.{category}";
try
{
return GetMethodsFromClass( className );
}
catch
{
return Array.Empty<string>();
}
}
private void UpdateTerrain()
{
if ( _heightmap is null ) return;
var ActiveScene = Editor.SceneEditorSession.Active.Scene;
var FirstTerrain = ActiveScene.GetAllComponents<Terrain>().FirstOrDefault();
if ( !FirstTerrain.IsValid() ) return;
int res = _heightmap.GetLength( 0 );
// Resize the scene terrain's storage to match the generated heightmap
if ( FirstTerrain.Storage is null )
FirstTerrain.Storage = new TerrainStorage { EmbeddedResource = new Sandbox.Resources.EmbeddedResource { ResourceCompiler = "embed" } };
FirstTerrain.Storage.SetResolution( res );
// Write the heightmap with the same indexing the preview uses (heightArray[y * res + x] =
// heightmap[x, y]). ConvertFloatArrayToUShortArray stores a transpose, which would mirror
// the terrain against the splatmap and misalign the materials on slopes.
ushort[] heightArray = new ushort[res * res];
for ( int y = 0; y < res; y++ )
{
for ( int x = 0; x < res; x++ )
{
float h = Math.Clamp( _heightmap[x, y], 0f, 1f );
heightArray[y * res + x] = (ushort)Math.Clamp( (int)(h * 65535f), 0, 65535 );
}
}
FirstTerrain.Storage.HeightMap = heightArray;
// Apply the splatmap as a control map so the material blending matches the generated
// splatmap. SetResolution wipes the control map, so we always rewrite it here. The splatmap
// is recomputed from the current settings so dispersion/layer changes made after Generate
// are honoured.
_splatmap = BuildTileGridSplatmap( _heightmap, TerrainGridSize,
(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );
if ( _splatmap != null )
{
// Resolve which materials to use: the assigned preview materials, else the terrain's
// existing materials, else fall back to loading local tmats.
var materials = _previewMaterials;
if ( materials == null || materials.Length == 0 )
materials = FirstTerrain.Storage.Materials?.ToArray();
if ( materials == null || materials.Length == 0 )
materials = LoadTerrainMaterialsSync();
if ( materials != null && materials.Length > 0 )
{
uint[] controlMap = new uint[res * res];
int matCount = materials.Length;
for ( int y = 0; y < res; y++ )
{
for ( int x = 0; x < res; x++ )
{
float layerPos = Math.Clamp( _splatmap[x, y], 0f, matCount - 1f );
int baseId = (int)MathF.Floor( layerPos );
int overlayId = Math.Min( baseId + 1, matCount - 1 );
byte blend = (byte)Math.Clamp( (int)((layerPos - baseId) * 255f), 0, 255 );
controlMap[y * res + x] = new CompactTerrainMaterial( (byte)baseId, (byte)overlayId, blend, false ).Packed;
}
}
FirstTerrain.Storage.ControlMap = controlMap;
FirstTerrain.Storage.Materials.Clear();
FirstTerrain.Storage.Materials.AddRange( materials );
}
}
FirstTerrain.Create();
FirstTerrain.SyncGPUTexture();
FirstTerrain.UpdateMaterialsBuffer();
}
/// <summary>
/// Synchronously loads usable local .tmat terrain materials so Apply can set the splat
/// control map even when the user never clicked "Randomize Materials".
/// </summary>
TerrainMaterial[] LoadTerrainMaterialsSync()
{
try
{
if ( _localTmatAssets is null || _localTmatAssets.Count == 0 )
{
var allLocal = Editor.AssetSystem.All
.Where( a => a is not null && !a.IsDeleted && !a.IsCloud )
.Where( a => (a.RelativePath?.EndsWith( ".tmat" ) ?? false) )
.ToList();
var with1k = allLocal.Where( a => a.RelativePath.Contains( "_1k" ) ).ToList();
_localTmatAssets = with1k.Count > 0 ? with1k : allLocal;
}
var pool = new List<Editor.Asset>( _localTmatAssets );
var materials = new List<TerrainMaterial>();
int layerCount = MaxTileSplatLayers();
while ( materials.Count < layerCount && pool.Count > 0 )
{
int idx = Random.Shared.Next( pool.Count );
var asset = pool[idx];
pool.RemoveAt( idx );
if ( !asset.TryLoadResource<TerrainMaterial>( out var found ) || found is null )
continue;
if ( !IsMaterialUsable( found, asset.Path ) )
continue;
materials.Add( found );
}
return materials.Count > 0 ? materials.ToArray() : null;
}
catch ( System.Exception e )
{
Log.Error( $"Failed to load terrain materials for apply: {e.Message}" );
return null;
}
}
/// <summary>
/// Applies per-cell mode: spawns one Terrain per grid cell, each at full resolution and
/// positioned so they tile together in the scene.
/// </summary>
private void UpdatePerCellTerrains()
{
if ( _cellHeightmaps == null || _cellHeightmaps.Count == 0 ) return;
var ActiveScene = Editor.SceneEditorSession.Active.Scene;
// Match the size/height of an existing terrain in the scene so the cells line up,
// falling back to the preview constants if the scene has no terrain yet.
var existing = ActiveScene.GetAllComponents<Terrain>().FirstOrDefault();
float cellSize = existing.IsValid() ? existing.TerrainSize : PreviewTerrainSize;
float terrainHeight = existing.IsValid() ? existing.TerrainHeight : PreviewTerrainHeight;
int grid = Math.Max( TerrainGridSize, 1 );
// Terrain size is a property on the component; size each cell so the whole grid spans cellSize*grid.
float sizePerCell = cellSize;
int cellRes = _cellHeightmaps[0].GetLength( 0 );
// Recompute the per-cell splatmaps from the current settings so dispersion/layer changes
// made after Generate are honoured.
_cellSplatmaps = BuildPerCellSplatmaps( _cellHeightmaps,
(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );
using ( ActiveScene.Push() )
{
for ( int ty = 0; ty < grid; ty++ )
{
for ( int tx = 0; tx < grid; tx++ )
{
int index = ty * grid + tx;
if ( index >= _cellHeightmaps.Count ) continue;
var go = new GameObject( true, $"terrain cell {tx},{ty}" );
var terrain = go.AddComponent<Terrain>( false );
var storage = new TerrainStorage();
storage.EmbeddedResource = new Sandbox.Resources.EmbeddedResource { ResourceCompiler = "embed" };
storage.SetResolution( cellRes );
storage.TerrainSize = sizePerCell;
storage.TerrainHeight = terrainHeight;
// Match the preview's indexing (heightArray[y * res + x] = map[x, y]) so the
// heightmap and splatmap line up on slopes.
ushort[] cellHeight = new ushort[cellRes * cellRes];
var cellMap = _cellHeightmaps[index];
for ( int y = 0; y < cellRes; y++ )
{
for ( int x = 0; x < cellRes; x++ )
{
float h = Math.Clamp( cellMap[x, y], 0f, 1f );
cellHeight[y * cellRes + x] = (ushort)Math.Clamp( (int)(h * 65535f), 0, 65535 );
}
}
storage.HeightMap = cellHeight;
// Add the splat control map so the material blending matches the generated splatmap.
// These are fresh cells, so resolve materials the same way as the combined apply.
if ( _cellSplatmaps != null && index < _cellSplatmaps.Count )
{
var materials = _previewMaterials;
if ( materials == null || materials.Length == 0 )
materials = LoadTerrainMaterialsSync();
if ( materials != null && materials.Length > 0 )
{
uint[] controlMap = new uint[cellRes * cellRes];
int matCount = materials.Length;
var splatmap = _cellSplatmaps[index];
for ( int y = 0; y < cellRes; y++ )
{
for ( int x = 0; x < cellRes; x++ )
{
float layerPos = Math.Clamp( splatmap[x, y], 0f, matCount - 1f );
int baseId = (int)MathF.Floor( layerPos );
int overlayId = Math.Min( baseId + 1, matCount - 1 );
byte blend = (byte)Math.Clamp( (int)((layerPos - baseId) * 255f), 0, 255 );
controlMap[y * cellRes + x] = new CompactTerrainMaterial( (byte)baseId, (byte)overlayId, blend, false ).Packed;
}
}
storage.ControlMap = controlMap;
storage.Materials.Clear();
storage.Materials.AddRange( materials );
}
}
terrain.Storage = storage;
terrain.TerrainSize = sizePerCell;
terrain.TerrainHeight = terrainHeight;
// Each cell is sizePerCell wide - tile them so the whole grid spans cellSize*grid
go.WorldPosition = new Vector3( tx * sizePerCell, ty * sizePerCell, 0f );
terrain.Create();
terrain.SyncGPUTexture();
terrain.UpdateMaterialsBuffer();
}
}
}
}
private float[,] AddStagingSquare( float[,] heightmap, int squareSize, float squareHeight, float centerX, float centerY )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
// Calculate the center and bounds of the square
int centerXPixel = (int)(centerX * width);
int centerYPixel = (int)(centerY * height);
int halfSize = squareSize / 2;
int startX = Math.Max( centerXPixel - halfSize, 0 );
int startY = Math.Max( centerYPixel - halfSize, 0 );
int endX = Math.Min( centerXPixel + halfSize, width - 1 );
int endY = Math.Min( centerYPixel + halfSize, height - 1 );
// Set the height values inside the square to be completely flat
for ( int y = startY; y <= endY; y++ )
{
for ( int x = startX; x <= endX; x++ )
{
heightmap[x, y] = squareHeight;
}
}
// Add the slope around the square
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
// Skip the flat square area
if ( x >= startX && x <= endX && y >= startY && y <= endY )
continue;
// Calculate the distance to the nearest edge of the square
int dx = Math.Max( Math.Abs( x - centerXPixel ) - halfSize, 0 );
int dy = Math.Max( Math.Abs( y - centerYPixel ) - halfSize, 0 );
float distanceToSquare = MathF.Sqrt( dx * dx + dy * dy );
// Calculate the target height for the slope
float slopeHeight = squareHeight - (distanceToSquare * 0.0038f); // 0.0038f is perfect for players
// Ensure the slope transitions smoothly into the existing terrain
heightmap[x, y] = Math.Max( heightmap[x, y], slopeHeight );
}
}
return heightmap;
}
private void GeneratePreviewFile( string path, out SKBitmap image, out SKBitmap splat )
{
//Create TerrainGenerationTool folder if it doesn't exist.
Directory.CreateDirectory( path );
string previewfile = Path.Combine( path, $"TerrainGenerationUtility_preview.png" );
string splatfile = Path.Combine( path, $"TerrainGenerationUtility_splat_preview.png" );
image = HeightmapToBitMap( _heightmap );
SaveImage( image, previewfile );
splat = SplatmapToBitMap( _splatmap, _splatcolors );
SaveSplatmapAsPng( splat, splatfile );
}
/// <summary>
/// Builds a fresh GPU texture from a bitmap so the preview widgets always get new data
/// (the resource cache would otherwise return the same stale texture for the same path).
/// </summary>
Texture TextureFromBitmap( SKBitmap bitmap )
{
if ( bitmap is null ) return Texture.Invalid;
int width = bitmap.Width;
int height = bitmap.Height;
byte[] rgba = new byte[width * height * 4];
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
var c = bitmap.GetPixel( x, y );
int i = (y * width + x) * 4;
rgba[i + 0] = c.Red;
rgba[i + 1] = c.Green;
rgba[i + 2] = c.Blue;
rgba[i + 3] = c.Alpha;
}
}
return Texture.Create( width, height )
.WithName( $"TerrainGenerationPreview_{Environment.TickCount}" )
.WithData( rgba )
.Finish();
}
private void GenerateImageFiles( string output_path )
{
string UsingDomainWarping = "";
string UsingErosionEmulation = "";
string UsingWaterCarving = "";
if ( DomainWarping )
{
UsingDomainWarping = "_warp";
}
if ( ErosionSimulation )
{
UsingErosionEmulation = "_erosion";
}
if ( RiverCarvingBool )
{
UsingWaterCarving = "_watercarving";
}
//Create TerrainGenerationTool folder if it doesn't exist.
Directory.CreateDirectory( output_path );
if ( GridStorage == GridStorageMode.PerCell && _cellHeightmaps != null && _cellHeightmaps.Count > 0 )
{
GeneratePerCellFiles( output_path );
return;
}
string rawfile = Path.Combine( output_path, $"TerrainGenerationUtility_export_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.raw" );
string previewfile = Path.Combine( output_path, $"TerrainGenerationUtility_preview_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png" );
string splatfile = Path.Combine( output_path, $"TerrainGenerationUtility_splat_export_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png" );
//Export RAW HeightMap file
SaveRaw( _heightmap, rawfile );
Log.Info( $"Raw file generated! - {rawfile}" );
//Generate & Export Preview image for widget
SKBitmap image = HeightmapToBitMap( _heightmap );
SaveImage( image, previewfile );
Log.Info( $"HeightMap preview file generated! - {previewfile}" );
//Generate & Export SplatMap image
float[,] splatmap = BuildTileGridSplatmap( _heightmap, TerrainGridSize,
(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );
SKBitmap splat = SplatmapToBitMap( splatmap, _splatcolors );
SaveSplatmapAsPng( splat, splatfile );
Log.Info( $"Splatmap file generated! - {splatfile}" );
// Split the layers across the requested number of splat maps (based on the first tile's settings)
int layerCount = _tileSplatLayerCounts != null && _tileSplatLayerCounts.Length > 0 ? Math.Max( _tileSplatLayerCounts[0], 2 ) : 2;
int mapCount = _tileSplatMapCounts != null && _tileSplatMapCounts.Length > 0 ? Math.Max( _tileSplatMapCounts[0], 1 ) : 1;
for ( int m = 0; m < mapCount; m++ )
{
int startLayer = m * layerCount / mapCount;
int endLayer = (m + 1) * layerCount / mapCount;
var mapBitmap = new SKBitmap( splatmap.GetLength( 0 ), splatmap.GetLength( 1 ) );
for ( int y = 0; y < mapBitmap.Height; y++ )
{
for ( int x = 0; x < mapBitmap.Width; x++ )
{
float layerPos = Math.Clamp( splatmap[x, y], 0f, layerCount - 1f );
int layer = (int)MathF.Round( layerPos );
if ( layer >= startLayer && layer < endLayer )
{
float local = (layer - startLayer) / (float)Math.Max( endLayer - startLayer, 1 );
var color = SplatMapGradient.Evaluate( Math.Clamp( local, 0f, 1f ) ).ToColor32();
mapBitmap.SetPixel( x, y, new SKColor( color.r, color.g, color.b, color.a ) );
}
else
{
mapBitmap.SetPixel( x, y, new SKColor( 0, 0, 0, 255 ) );
}
}
}
string mapFile = Path.Combine( output_path, $"TerrainGenerationUtility_splatmap_{m}_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png" );
SaveSplatmapAsPng( mapBitmap, mapFile );
Log.Info( $"Splatmap {m} file generated! - {mapFile}" );
}
Log.Info( $"All export files saved! {output_path}" );
}
/// <summary>
/// Exports each grid cell as its own full-resolution .raw heightmap and .png splatmap,
/// named by grid coordinate.
/// </summary>
private void GeneratePerCellFiles( string output_path )
{
Directory.CreateDirectory( output_path );
int grid = Math.Max( TerrainGridSize, 1 );
for ( int ty = 0; ty < grid; ty++ )
{
for ( int tx = 0; tx < grid; tx++ )
{
int index = ty * grid + tx;
if ( index >= _cellHeightmaps.Count ) continue;
var heightmap = _cellHeightmaps[index];
string rawfile = Path.Combine( output_path, $"TerrainGenerationUtility_cell_{tx}_{ty}.raw" );
SaveRaw( heightmap, rawfile );
Log.Info( $"Cell {tx},{ty} raw file generated! - {rawfile}" );
SKBitmap image = HeightmapToBitMap( heightmap );
string previewfile = Path.Combine( output_path, $"TerrainGenerationUtility_cell_{tx}_{ty}_preview.png" );
SaveImage( image, previewfile );
Log.Info( $"Cell {tx},{ty} preview generated! - {previewfile}" );
if ( _cellSplatmaps != null && index < _cellSplatmaps.Count )
{
SKBitmap splat = SplatmapToBitMap( _cellSplatmaps[index], _splatcolors );
string splatfile = Path.Combine( output_path, $"TerrainGenerationUtility_cell_{tx}_{ty}_splat.png" );
SaveSplatmapAsPng( splat, splatfile );
Log.Info( $"Cell {tx},{ty} splatmap generated! - {splatfile}" );
}
}
}
Log.Info( $"All per-cell export files saved! {output_path}" );
}
public float[,] GenerateHeightmap( int width, int height, Func<int, int, float> generator, float maxHeight, int smoothpasses )
{
float[,] heightmap = new float[width, height];
float actualMaxHeight = float.MinValue;
// Use parallel processing to generate heightmap
object maxLock = new object(); // Lock object for thread safety
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float value = generator( x, y );
heightmap[x, y] = value;
// Update actual max height (thread-safe)
lock ( maxLock )
{
if ( value > actualMaxHeight )
{
actualMaxHeight = value;
}
}
}
} );
// Scale all values by the actual max height and up to the specified max height
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] = (heightmap[x, y] / actualMaxHeight) * maxHeight;
}
} );
// Apply smoothing if needed
if ( smoothpasses > 0 )
{
return SmoothHeightmap( heightmap, smoothpasses );
}
else
{
return heightmap;
}
}
public static float[,] GenerateStackedNoise(
int width,
int height,
long seed,
int layers,
float initialFrequency,
float frequencyMultiplier,
float initialAmplitude,
float amplitudeMultiplier,
Func<int, int, float> shapeFunction, // Shape function applied after stacking noise
float maxHeight,
int smoothingPasses,
float terrainPlaneScale // New variable to scale the noise
)
{
// Initialize the heightmap with zeros
float[,] heightmap = new float[width, height];
// Random offset generator for noise layers
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float[] xOffsets = new float[layers];
float[] yOffsets = new float[layers];
for ( int i = 0; i < layers; i++ )
{
xOffsets[i] = random.Next( -100000, 100000 ) / 1000.0f;
yOffsets[i] = random.Next( -100000, 100000 ) / 1000.0f;
}
// Adjust frequency based on TerrainPlaneScale
float scaleFactor = Math.Clamp( terrainPlaneScale, 0.01f, 1.0f );
// Multithreaded noise generation
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float value = 0.0f;
for ( int layer = 0; layer < layers; layer++ )
{
float frequency = initialFrequency * MathF.Pow( frequencyMultiplier, layer ) / scaleFactor;
float amplitude = initialAmplitude * MathF.Pow( amplitudeMultiplier, layer );
// Normalized coordinates adjusted by scale factor
float nx = (x / (float)width) * frequency;
float ny = (y / (float)height) * frequency;
// Apply random offsets
nx += xOffsets[layer];
ny += yOffsets[layer];
// Generate noise
float noiseValue = OpenSimplex2S.Noise2( (seed + layer) & 0xFFFFFFFF, nx, ny );
value += Math.Clamp( noiseValue, -1.0f, 1.0f ) * amplitude;
}
// Save the computed value to the heightmap
// (each Parallel.For iteration writes its own distinct row - no lock needed)
heightmap[x, y] += value;
}
} );
// Normalize the heightmap to the range [0, 1]
heightmap = NormalizeHeightmap( heightmap );
// Apply the shape function and amplify its contribution if needed
float shapeAmplification = 1.2f; // Adjust for stronger shape effects
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] *= MathF.Pow( shapeFunction( x, y ), shapeAmplification );
}
} );
// Rescale the heightmap to the desired maxHeight
float currentMax = FindMaxHeight( heightmap );
if ( currentMax > 0 )
{
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] = (heightmap[x, y] / currentMax) * maxHeight;
}
} );
}
// Apply smoothing
if ( smoothingPasses > 0 )
{
heightmap = SmoothHeightmap( heightmap, smoothingPasses );
}
return heightmap;
}
// Helper method to find the maximum height in a heightmap
private static float FindMaxHeight( float[,] heightmap )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float max = float.MinValue;
object maxLock = new object();
Parallel.For( 0, height, y =>
{
float rowMax = float.MinValue;
for ( int x = 0; x < width; x++ )
{
if ( heightmap[x, y] > rowMax )
{
rowMax = heightmap[x, y];
}
}
if ( rowMax > max )
{
lock ( maxLock )
{
if ( rowMax > max ) max = rowMax;
}
}
} );
return max;
}
private static float[,] NormalizeHeightmap( float[,] heightmap )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
// Find the min and max values (parallel, per-row reduce)
object lockObject = new object();
float min = float.MaxValue;
float max = float.MinValue;
Parallel.For( 0, height, y =>
{
float rowMin = float.MaxValue;
float rowMax = float.MinValue;
for ( int x = 0; x < width; x++ )
{
float value = heightmap[x, y];
if ( value < rowMin ) rowMin = value;
if ( value > rowMax ) rowMax = value;
}
lock ( lockObject )
{
if ( rowMin < min ) min = rowMin;
if ( rowMax > max ) max = rowMax;
}
} );
float range = max - min;
if ( range <= 0f ) range = 1f;
// Normalize the values (parallel, independent writes)
float[,] normalized = new float[width, height];
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
normalized[x, y] = (heightmap[x, y] - min) / range;
}
} );
return normalized;
}
public static float[,] AddTurbulenceForRivers(
float[,] heightmap,
long seed,
float riverFrequency, // Frequency for river placement
float riverWidth, // Width of the rivers
float riverDepth, // Depth of the rivers
float turbulenceFrequency, // Turbulence frequency
float turbulenceStrength, // Turbulence strength
float minRiverSpacing, // Minimum spacing between rivers
float slopeSteepness, // Controls the gradual slope of the riverbanks
float terrainNoiseFrequency, // Matches terrain surface noise frequency
float terrainNoiseAmplitude // Matches terrain surface noise amplitude
)
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float[,] newHeightmap = (float[,])heightmap.Clone();
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float[,] riverPlacementNoise = new float[width, height];
// Generate river placement noise
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float nx = x / (float)width;
float ny = y / (float)height;
// Noise for river placement
riverPlacementNoise[x, y] = OpenSimplex2S.Noise2( seed, nx * riverFrequency, ny * riverFrequency );
}
} );
// Process heightmap with river carving
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float nx = x / (float)width;
float ny = y / (float)height;
float riverNoise = MathF.Abs( riverPlacementNoise[x, y] ); // Use absolute noise for placement
// Determine if the point is within the river carving zone
if ( riverNoise < riverWidth )
{
// Calculate the smooth curve effect based on distance from the center
float distanceFactor = 1.0f - (riverNoise / riverWidth); // 1 at center, 0 at edge
float smoothDepthReduction = MathF.Pow( distanceFactor, slopeSteepness ) * riverDepth;
// Add turbulence for a more organic flow
float turbulence = OpenSimplex2S.Noise2( seed + 1, nx * turbulenceFrequency, ny * turbulenceFrequency )
* turbulenceStrength;
// Apply smooth depth reduction and turbulence
float reducedHeight = newHeightmap[x, y] - smoothDepthReduction + turbulence;
// Clamp height to ensure it doesn't rise above the original
newHeightmap[x, y] = MathF.Max( 0, MathF.Min( newHeightmap[x, y], reducedHeight ) );
}
// Enforce minimum spacing between rivers
if ( riverNoise < minRiverSpacing )
{
// Slightly raise the terrain to enforce separation
newHeightmap[x, y] += (minRiverSpacing - riverNoise) * 0.05f;
}
}
} );
// Add base noise to the entire heightmap after carving
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float nx = x / (float)width;
float ny = y / (float)height;
// Generate base noise
float baseNoise = OpenSimplex2S.Noise2( seed + 2, nx * 6f, ny * 6f )
* 0.02f;
// Add noise to the heightmap
newHeightmap[x, y] = MathF.Max( 0, newHeightmap[x, y] + baseNoise );
}
} );
return newHeightmap;
}
// Smooths the heightmap using a simple box blur with adjustable strength
private static float[,] SmoothHeightmap( float[,] heightmap, int smoothingPasses )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float[,] smoothed = new float[width, height];
for ( int pass = 0; pass < smoothingPasses; pass++ )
{
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float sum = 0;
int count = 0;
// Iterate through neighbors
for ( int dy = -1; dy <= 1; dy++ )
{
for ( int dx = -1; dx <= 1; dx++ )
{
int nx = x + dx;
int ny = y + dy;
if ( nx >= 0 && nx < width && ny >= 0 && ny < height )
{
sum += heightmap[nx, ny];
count++;
}
}
}
smoothed[x, y] = sum / count;
}
} );
// Copy smoothed values back to the original heightmap for the next pass
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] = smoothed[x, y];
}
} );
}
return smoothed;
}
public static ushort[] ConvertFloatArrayToUShortArray( float[,] input, float scale = 65535.0f )
{
// Get the dimensions of the 2D array
int rows = input.GetLength( 0 );
int cols = input.GetLength( 1 );
// Initialize the 1D ushort array
ushort[] output = new ushort[rows * cols];
// Iterate over the 2D array row by row
int index = 0;
for ( int row = 0; row < rows; row++ )
{
for ( int col = 0; col < cols; col++ )
{
// Convert the float to ushort, scaling if necessary
float value = input[row, col];
value = Math.Clamp( value, 0.0f, 1.0f ); // Ensure the float is in the 0 to 1 range
output[index++] = (ushort)(value * scale);
}
}
return output;
}
public static byte[] ConvertRawFloatArrayToByteArray( float[,] rawData, float scale = 65535.0f )
{
if ( rawData == null )
{
throw new ArgumentNullException( nameof( rawData ), "Input rawData cannot be null." );
}
int rows = rawData.GetLength( 0 );
int cols = rawData.GetLength( 1 );
// Create a byte array with 2 bytes per value
byte[] byteArray = new byte[rows * cols * 2]; // 2 bytes per ushort
int index = 0;
for ( int row = 0; row < rows; row++ )
{
for ( int col = 0; col < cols; col++ )
{
float value = rawData[row, col];
value = Math.Clamp( value, 0.0f, 1.0f ); // Ensure value is in the range [0, 1]
// Convert to 16-bit unsigned integer
ushort ushortValue = (ushort)(value * scale);
// Store in byte array (little-endian order)
byteArray[index++] = (byte)(ushortValue & 0xFF); // Lower byte
byteArray[index++] = (byte)((ushortValue >> 8) & 0xFF); // Upper byte
}
}
return byteArray;
}
// Converts a heightmap to a grayscale image using SkiaSharp
public static SKBitmap HeightmapToBitMap( float[,] heightmap )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
SKBitmap bitmap = new SKBitmap( width, height );
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
int intensity = (int)(heightmap[x, y] * 255);
intensity = Math.Clamp( intensity, 0, 255 );
bitmap.SetPixel( x, y, new SKColor( (byte)intensity, (byte)intensity, (byte)intensity ) );
}
}
return bitmap;
}
public byte[] ConvertSKBitmapToBytes( SKBitmap bitmap, SKEncodedImageFormat format, int quality = 100 )
{
// Create an SKImage from the SKBitmap
using ( var image = SKImage.FromBitmap( bitmap ) )
{
// Encode the image to the desired format (e.g., PNG, JPEG)
using ( var data = image.Encode( format, quality ) )
{
// Convert SKData to a byte array
return data.ToArray();
}
}
}
public static void SaveImage(
SKBitmap bitmap,
string filename,
float rotationDegrees = 270f,
bool reverseHorizontal = false,
bool reverseVertical = true
)
{
int width = bitmap.Width;
int height = bitmap.Height;
// Create a new bitmap to hold the transformed image
using var transformedBitmap = new SKBitmap( width, height );
// Create a canvas to draw the transformed image
using var canvas = new SKCanvas( transformedBitmap );
// Clear the canvas with transparency
canvas.Clear( SKColors.Transparent );
// Apply transformations
canvas.Save();
// Translate to the center of the canvas for rotation and flipping
canvas.Translate( width / 2f, height / 2f );
// Apply flipping first
float scaleX = reverseHorizontal ? -1f : 1f;
float scaleY = reverseVertical ? -1f : 1f;
canvas.Scale( scaleX, scaleY );
// Apply rotation
if ( rotationDegrees != 0 )
{
canvas.RotateDegrees( rotationDegrees );
}
// Translate back to ensure the image is drawn correctly
canvas.Translate( -width / 2f, -height / 2f );
// Draw the original bitmap onto the transformed canvas
canvas.DrawBitmap( bitmap, 0, 0 );
// Restore the canvas to finalize the transformations
canvas.Restore();
// Flush the canvas
canvas.Flush();
// Save the transformed bitmap as a PNG file
using var pixmap = transformedBitmap.PeekPixels();
using var image = SKImage.FromPixels( pixmap );
using var data = image.Encode( SKEncodedImageFormat.Png, 100 );
using var stream = File.OpenWrite( filename );
data.SaveTo( stream );
}
public static void SaveRaw( float[,] heightmap, string filename, int rotationDegrees = 270, bool reverseHorizontal = true, bool reverseVertical = false )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
// Rotate the heightmap if requested
if ( rotationDegrees != 0 )
{
heightmap = RotateHeightmap( heightmap, rotationDegrees );
if ( rotationDegrees == 90 || rotationDegrees == 270 )
{
// Swap width and height for 90° or 270° rotations
(width, height) = (height, width);
}
}
// Reverse the heightmap if requested
if ( reverseHorizontal || reverseVertical )
{
heightmap = ReverseHeightmap( heightmap, reverseHorizontal, reverseVertical );
}
using var fileStream = new FileStream( filename, FileMode.Create, FileAccess.Write );
using var binaryWriter = new BinaryWriter( fileStream );
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
// Scale image data to 16-bit
ushort value = (ushort)(Math.Clamp( heightmap[x, y], 0, 1 ) * 65535);
binaryWriter.Write( value );
}
}
}
// Helper method to rotate the heightmap by 90°, 180°, or 270°
private static float[,] RotateHeightmap( float[,] original, int rotationDegrees )
{
int originalWidth = original.GetLength( 0 );
int originalHeight = original.GetLength( 1 );
float[,] rotated;
switch ( rotationDegrees )
{
case 90:
rotated = new float[originalHeight, originalWidth];
for ( int y = 0; y < originalHeight; y++ )
{
for ( int x = 0; x < originalWidth; x++ )
{
rotated[y, originalWidth - 1 - x] = original[x, y];
}
}
break;
case 180:
rotated = new float[originalWidth, originalHeight];
for ( int y = 0; y < originalHeight; y++ )
{
for ( int x = 0; x < originalWidth; x++ )
{
rotated[originalWidth - 1 - x, originalHeight - 1 - y] = original[x, y];
}
}
break;
case 270:
rotated = new float[originalHeight, originalWidth];
for ( int y = 0; y < originalHeight; y++ )
{
for ( int x = 0; x < originalWidth; x++ )
{
rotated[originalHeight - 1 - y, x] = original[x, y];
}
}
break;
default:
throw new ArgumentException( "Rotation must be 0, 90, 180, or 270 degrees." );
}
return rotated;
}
// Helper method to reverse the heightmap horizontally and/or vertically
private static float[,] ReverseHeightmap( float[,] original, bool reverseHorizontal, bool reverseVertical )
{
int width = original.GetLength( 0 );
int height = original.GetLength( 1 );
float[,] reversed = new float[width, height];
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
int targetX = reverseHorizontal ? width - 1 - x : x;
int targetY = reverseVertical ? height - 1 - y : y;
reversed[targetX, targetY] = original[x, y];
}
}
return reversed;
}
public static float[,] GenerateSplatmap( float[,] heightmap, float[] thresholds, float maxHeight, int layerCount = -1, SplatDispersionMode dispersion = SplatDispersionMode.Evenly, float blendStrength = 0.35f )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
int layers = layerCount > 0 ? layerCount : Math.Max( thresholds.Length, 2 );
float[,] splatmap = new float[width, height];
// Build normalized height bounds from the data itself (robust to maxHeight being lower than peaks)
float minH = float.MaxValue, maxH = float.MinValue;
object minMaxLock = new object();
Parallel.For( 0, height, y =>
{
float rowMin = float.MaxValue;
float rowMax = float.MinValue;
for ( int x = 0; x < width; x++ )
{
if ( heightmap[x, y] < rowMin ) rowMin = heightmap[x, y];
if ( heightmap[x, y] > rowMax ) rowMax = heightmap[x, y];
}
lock ( minMaxLock )
{
if ( rowMin < minH ) minH = rowMin;
if ( rowMax > maxH ) maxH = rowMax;
}
} );
float range = MathF.Max( maxH - minH, 0.0001f );
// Thresholds are the height positions of each color stop in [0,1].
// Evenly mode uses equally spaced stops; Natural mode uses a slope-weighted
// distribution so colors bunch on flat/common terrain and spread on steep slopes.
float[] stops = thresholds;
if ( dispersion == SplatDispersionMode.Natural )
{
stops = ComputeNaturalThresholds( heightmap, layers );
}
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
// Normalized height in [0,1]
float normalizedHeight = Math.Clamp( (heightmap[x, y] - minH) / range, 0f, 1f );
// Interpolated layer position from the color stop positions
float layerPos = HeightToLayer( normalizedHeight, stops );
// Soft snap to the nearest layer governed by blend strength
float center = MathF.Round( layerPos );
float distance = layerPos - center;
float factor;
if ( MathF.Abs( distance ) <= blendStrength * 0.5f )
{
factor = layerPos;
}
else
{
factor = center;
}
splatmap[x, y] = Math.Clamp( factor, 0f, layers - 1 );
}
} );
return splatmap;
}
static float HeightToLayer( float normalizedHeight, float[] stops )
{
int count = stops.Length;
if ( count <= 1 ) return 0f;
if ( normalizedHeight <= stops[0] ) return 0f;
if ( normalizedHeight >= stops[count - 1] ) return count - 1f;
for ( int i = 0; i < count - 1; i++ )
{
if ( normalizedHeight >= stops[i] && normalizedHeight <= stops[i + 1] )
{
float t = (normalizedHeight - stops[i]) / MathF.Max( stops[i + 1] - stops[i], 0.0001f );
return i + t;
}
}
return count - 1f;
}
/// <summary>
/// Places color stop thresholds based on the terrain's slope-weighted height distribution.
/// Flat, common heights get many stops (lots of color blending); steep, rare heights get few
/// stops (few color changes), matching how terrain materials naturally appear.
/// </summary>
static float[] ComputeNaturalThresholds( float[,] heightmap, int layerCount )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float minH = float.MaxValue, maxH = float.MinValue;
object minMaxLock = new object();
Parallel.For( 0, height, y =>
{
float rowMin = float.MaxValue;
float rowMax = float.MinValue;
for ( int x = 0; x < width; x++ )
{
if ( heightmap[x, y] < rowMin ) rowMin = heightmap[x, y];
if ( heightmap[x, y] > rowMax ) rowMax = heightmap[x, y];
}
lock ( minMaxLock )
{
if ( rowMin < minH ) minH = rowMin;
if ( rowMax > maxH ) maxH = rowMax;
}
} );
float range = MathF.Max( maxH - minH, 0.0001f );
// Histogram of normalized heights, weighted by flatness (1 - slope).
// Use a per-thread local histogram, then merge, to avoid lock contention.
const int bins = 128;
float[] hist = new float[bins];
Parallel.For( 0, height, y =>
{
float[] localHist = new float[bins];
for ( int x = 0; x < width; x++ )
{
float h = heightmap[x, y];
float hL = heightmap[Math.Max( x - 1, 0 ), y];
float hR = heightmap[Math.Min( x + 1, width - 1 ), y];
float hD = heightmap[x, Math.Max( y - 1, 0 )];
float hU = heightmap[x, Math.Min( y + 1, height - 1 )];
float localDiff = (MathF.Abs( hR - hL ) + MathF.Abs( hU - hD )) * 0.5f;
float slope = Math.Clamp( localDiff / MathF.Max( range * 0.1f, 0.0001f ), 0f, 1f );
float weight = MathF.Max( 1f - slope, 0.05f );
float normalizedHeight = Math.Clamp( (h - minH) / range, 0f, 1f );
int bin = Math.Clamp( (int)(normalizedHeight * (bins - 1)), 0, bins - 1 );
localHist[bin] += weight;
}
lock ( minMaxLock )
{
for ( int i = 0; i < bins; i++ ) hist[i] += localHist[i];
}
} );
// Cumulative distribution
float total = hist.Sum();
if ( total <= 0f )
{
total = 1f;
for ( int i = 0; i < bins; i++ ) hist[i] = 1f;
}
float[] thresholds = new float[layerCount];
thresholds[0] = 0f;
thresholds[layerCount - 1] = 1f;
float cum = 0f;
int binIndex = 0;
for ( int i = 1; i < layerCount - 1; i++ )
{
float target = (i / (float)(layerCount - 1)) * total;
while ( binIndex < bins - 1 && cum < target )
{
cum += hist[binIndex];
binIndex++;
}
thresholds[i] = binIndex / (float)(bins - 1);
}
// Ensure monotonic
for ( int i = 1; i < layerCount; i++ )
{
thresholds[i] = MathF.Max( thresholds[i], thresholds[i - 1] );
}
return thresholds;
}
public static SKBitmap SplatmapToBitMap( float[,] splatmap, SKColor[] colors )
{
int width = splatmap.GetLength( 0 );
int height = splatmap.GetLength( 1 );
SKBitmap bitmap = new SKBitmap( width, height );
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
// Map the splatmap value to a valid layer position
float layerPos = Math.Clamp( splatmap[x, y], 0f, colors.Length - 1f );
int layer0 = (int)MathF.Floor( layerPos );
int layer1 = Math.Min( layer0 + 1, colors.Length - 1 );
float t = layerPos - layer0;
// Blend between the two nearest layer colors
var c0 = colors[layer0];
var c1 = colors[layer1];
var color = new SKColor(
(byte)MathX.LerpTo( c0.Red, c1.Red, t ),
(byte)MathX.LerpTo( c0.Green, c1.Green, t ),
(byte)MathX.LerpTo( c0.Blue, c1.Blue, t ),
(byte)MathX.LerpTo( c0.Alpha, c1.Alpha, t ) );
bitmap.SetPixel( x, y, color );
}
} );
return bitmap;
}
public static void SaveSplatmapAsPng(
SKBitmap bitmap,
string filename,
float rotationDegrees = 270f,
bool reverseHorizontal = false,
bool reverseVertical = true
)
{
int width = bitmap.Width;
int height = bitmap.Height;
using var transformedBitmap = new SKBitmap( width, height );
using var canvas = new SKCanvas( transformedBitmap );
// Clear the canvas with transparency
canvas.Clear( SKColors.Transparent );
// Apply transformations
canvas.Save();
// Translate to the center of the canvas for rotation and flipping
canvas.Translate( width / 2f, height / 2f );
// Apply flipping first
float scaleX = reverseHorizontal ? -1f : 1f;
float scaleY = reverseVertical ? -1f : 1f;
canvas.Scale( scaleX, scaleY );
// Apply rotation
if ( rotationDegrees != 0 )
{
canvas.RotateDegrees( rotationDegrees );
}
// Translate back to ensure the image is drawn correctly
canvas.Translate( -width / 2f, -height / 2f );
// Draw the original bitmap onto the transformed canvas
canvas.DrawBitmap( bitmap, 0, 0 );
// Restore the canvas to finalize the transformations
canvas.Restore();
// Flush the canvas
canvas.Flush();
// Save the transformed bitmap as a PNG file
using var pixmap = transformedBitmap.PeekPixels();
using var image = SKImage.FromPixels( pixmap );
using var data = image.Encode( SKEncodedImageFormat.Png, 100 );
using var stream = File.OpenWrite( filename );
data.SaveTo( stream );
}
}
/// <summary>
/// An icon + label picker whose options wrap onto multiple lines.
/// Mimics the interface of <see cref="Editor.SegmentedControl"/> (AddOption, Selected, SelectedIndex, OnSelectedChanged).
/// </summary>
public class WrapSelector : Widget
{
readonly List<WrapOption> _buttons = new();
readonly List<string> _names = new();
public string Selected
{
get
{
for ( int i = 0; i < _buttons.Count; i++ )
{
if ( _buttons[i].IsActive )
return _names[i];
}
return null;
}
set
{
SetSelected( value );
}
}
public int SelectedIndex
{
get
{
for ( int i = 0; i < _buttons.Count; i++ )
{
if ( _buttons[i].IsActive )
return i;
}
return -1;
}
set
{
if ( value >= 0 && value < _names.Count )
SetSelected( _names[value] );
}
}
public Action<string> OnSelectedChanged { get; set; }
public WrapSelector( Widget parent = null ) : base( parent )
{
Layout = Layout.Row();
Layout.Spacing = 4;
SetSizeMode( SizeMode.CanGrow, SizeMode.CanGrow );
HorizontalSizeMode = SizeMode.Flexible;
}
public void AddOption( string name, string icon = null, int? count = null, string label = null )
{
if ( _names.Contains( name ) ) return;
if ( string.IsNullOrEmpty( name ) )
{
// Special "clear" option, shown with the close icon
icon ??= "close";
}
else
{
icon ??= IconFor( name );
}
var option = new WrapOption( this, label ?? (string.IsNullOrEmpty( name ) ? "Clear" : name), icon );
option.IsActive = false;
option.MouseLeftPress = () => SetSelected( name );
_names.Add( name );
_buttons.Add( option );
Layout.Add( option );
}
public bool HasOption( string name ) => _names.Contains( name );
public new void DestroyChildren()
{
foreach ( var b in _buttons )
{
if ( b.IsValid() )
b.Destroy();
}
_buttons.Clear();
_names.Clear();
}
void SetSelected( string name )
{
bool changed = Selected != name;
for ( int i = 0; i < _buttons.Count; i++ )
{
_buttons[i].IsActive = _names[i] == name;
}
if ( changed )
{
OnSelectedChanged?.Invoke( name );
}
}
static string IconFor( string name )
{
switch ( name )
{
case "Islands": return "landscape";
case "Mountainous": return "terrain";
case "Planetary": return "public";
case "Realistic": return "photo";
case "Sea": return "water";
case "Volcanic": return "volcano";
case "Default": return "shapes";
case "Archipelagos": return "scatter_plot";
case "Atoll": return "crop_square";
case "Islets": return "blur_on";
case "Oceanic": return "waves";
case "Cliff": return "terrain";
case "Craters": return "brightness_low";
case "Hills": return "landscape";
case "Plateau": return "square_foot";
case "SeaBed": return "water";
case "Sharded": return "dashboard";
default: return "shapes";
}
}
}
/// <summary>
/// A single option in a <see cref="WrapSelector"/>: an icon with a text label underneath.
/// </summary>
public class WrapOption : Widget
{
public string Icon { get; }
public string Text { get; }
public bool IsActive { get; set; }
public WrapOption( Widget parent, string text, string icon ) : base( parent )
{
Text = text;
Icon = icon;
Cursor = CursorShape.Finger;
ToolTip = text;
MinimumSize = new Vector2( 52, 44 );
}
protected override Vector2 SizeHint()
{
Paint.SetDefaultFont( 7 );
var textRect = Paint.MeasureText( new Rect( 0, 0, 60, 100 ), Text, TextFlag.WordWrap );
return new Vector2( MathF.Max( textRect.Size.x + 10, 52 ), textRect.Size.y + 24 );
}
protected override void OnPaint()
{
base.OnPaint();
Paint.Antialiasing = true;
Paint.ClearPen();
var rect = LocalRect;
var background = IsActive ? Theme.Primary.WithAlpha( 0.25f ) : Theme.ControlBackground.WithAlpha( 0.6f );
if ( Paint.HasMouseOver ) background = background.Lighten( 0.1f );
Paint.SetBrush( background );
Paint.DrawRect( rect, Theme.ControlRadius );
var iconRect = new Rect( rect.Left, rect.Top + 4, rect.Width, rect.Height * 0.55f );
var color = IsActive ? Theme.Primary : Theme.Text.WithAlpha( 0.8f );
Paint.SetPen( color );
Paint.DrawIcon( iconRect, Icon, 18, TextFlag.Center );
var textRect = new Rect( rect.Left + 2, rect.Top + rect.Height * 0.55f, rect.Width - 4, rect.Height * 0.45f );
Paint.SetDefaultFont( 7 );
Paint.DrawText( textRect, Text, TextFlag.Center | TextFlag.WordWrap );
}
}
/// <summary>
/// A clickable box representing one terrain grid cell (or the "All" box). Shows the tile's
/// current category name and is highlighted when selected.
/// </summary>
public class TileGridBox : Widget
{
public int Index { get; }
public string Text { get; set; }
string _subtitle;
public bool IsSelected { get; set; }
public Action OnClicked { get; set; }
public TileGridBox( Widget parent, int index, string text, string subtitle = null ) : base( parent )
{
Index = index;
Text = text;
_subtitle = subtitle;
Cursor = CursorShape.Finger;
ToolTip = subtitle ?? text;
MinimumSize = new Vector2( 44, 40 );
MouseLeftPress = () => OnClicked?.Invoke();
}
protected override Vector2 SizeHint()
{
return new Vector2( 48, 44 );
}
protected override void OnPaint()
{
base.OnPaint();
Paint.Antialiasing = true;
Paint.ClearPen();
var rect = LocalRect;
var bg = IsSelected ? Theme.Primary.WithAlpha( 0.3f ) : Theme.ControlBackground.WithAlpha( 0.6f );
if ( Paint.HasMouseOver ) bg = bg.Lighten( 0.1f );
Paint.SetBrush( bg );
Paint.DrawRect( rect, Theme.ControlRadius );
if ( IsSelected )
{
Paint.SetPen( Theme.Primary, 2 );
Paint.DrawRect( rect, Theme.ControlRadius );
}
var textRect = new Rect( rect.Left + 3, rect.Top + 2, rect.Width - 6, rect.Height - 4 );
Paint.SetDefaultFont( 7 );
Paint.SetPen( IsSelected ? Theme.Primary : Theme.Text.WithAlpha( 0.9f ) );
Paint.DrawText( textRect, Text ?? "?", TextFlag.Center | TextFlag.WordWrap );
}
}
/// <summary>
/// A compact dropdown-style picker used in the tile grid. Shows a button with the current value
/// and opens a popup listing the available options.
/// </summary>
public class TileDropdownPicker : Widget
{
string[] _options;
Button _button;
string _label;
public string Selected { get; private set; }
public Action<string> OnPicked { get; set; }
public TileDropdownPicker( Widget parent, string label, string[] options ) : base( parent )
{
_label = label;
_options = options ?? Array.Empty<string>();
Layout = Layout.Column();
Layout.Spacing = 2;
var labelWidget = new Label( label );
labelWidget.SetStyles( "font-size: 9px; color: #999;" );
Layout.Add( labelWidget );
_button = new Button( Selected ?? "None", this );
_button.FixedHeight = Theme.RowHeight;
_button.Clicked += OpenMenu;
Layout.Add( _button );
}
public void SetOptions( string[] options )
{
_options = options ?? Array.Empty<string>();
}
public void SetSelected( string value )
{
Selected = value;
if ( _button != null && _button.IsValid() )
_button.Text = value ?? "None";
}
void OpenMenu()
{
var popup = new PopupWidget( null );
popup.Layout = Layout.Column();
popup.Layout.Margin = 4;
popup.Width = Math.Max( 180, _button.ScreenRect.Width );
var scroller = popup.Layout.Add( new ScrollArea( this ), 1 );
scroller.Canvas = new Widget( scroller )
{
Layout = Layout.Column(),
VerticalSizeMode = SizeMode.CanGrow | SizeMode.Expand
};
foreach ( var option in _options )
{
var item = scroller.Canvas.Layout.Add( new Button( option ) );
item.MouseLeftPress = () =>
{
SetSelected( option );
OnPicked?.Invoke( option );
popup.Close();
};
}
popup.Position = _button.ScreenRect.BottomLeft;
popup.Visible = true;
popup.AdjustSize();
popup.ConstrainToScreen();
}
protected override void OnPaint()
{
Paint.ClearPen();
Paint.SetBrush( Theme.ControlBackground );
Paint.DrawRect( LocalRect, Theme.ControlRadius );
base.OnPaint();
}
}
Editor
library
using Editor;
using Sandbox;
using System;
namespace Sturnus.TerrainGenerationTool;
public static class Realistic
{
public static float Default(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool domainWarping,
float domainWarpingSize,
float domainWarpingStrength)
{
// Normalize coordinates to [-1, 1]
float nx = (x / (float)width) * 2 - 1;
float ny = (y / (float)height) * 2 - 1;
// Apply domain warping for natural distortion
if ( domainWarping )
{
float warpX = OpenSimplex2S.Noise2( seed, nx * domainWarpingSize, ny * domainWarpingSize ) * domainWarpingStrength;
float warpY = OpenSimplex2S.Noise2( seed + 1, nx * domainWarpingSize, ny * domainWarpingSize ) * domainWarpingStrength;
nx += warpX;
ny += warpY;
}
float baseTerrain = OpenSimplex2S.Noise2( seed, nx, ny );
// Create hill/valley transitions with non-linear blending
float hillFactor = MathF.Pow( baseTerrain, 6 ); // Emphasize hills
float valleyFactor = 1.0f - MathF.Pow( 1.0f - baseTerrain, 1 ); // Emphasize valleys
// Use smoothstep-like function for non-linear blending
float smoothTransition = SmoothStep( 0.75f, 1.25f, baseTerrain );
float terrainShape = MathX.Lerp( valleyFactor, hillFactor, smoothTransition );
// Add finer details
float fineNoise = OpenSimplex2S.Noise2( seed + 2, nx * 8.0f, ny * 8.0f ) * 0.1f;
// Combine base terrain with fine details
float heightValue = terrainShape + fineNoise;
// Add a baseline value to ensure no flat zero areas
float baseline = minHeight; // Minimum height
heightValue = MathF.Max( heightValue, baseline );
// Normalize height to [0, 1]
return Math.Clamp( heightValue, 0.0f, 1.0f );
}
public static float Hills( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize = 0.1f, float warpStrength = 0.5f )
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float hillHeight = 0.6f; // Maximum height of the hills
float hillFrequency = 3f; // Frequency of the hills
float noiseStrength = 0f; // Strength of noise for natural detail
// Apply domain warping for irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Base overlapping hills
float hillBase1 = MathF.Sin( nx * hillFrequency * MathF.PI ) + MathF.Cos( ny * hillFrequency * MathF.PI );
float hillBase2 = MathF.Sin( ny * (hillFrequency * 0.75f) * MathF.PI ) + MathF.Cos( nx * (hillFrequency * 0.75f) * MathF.PI );
float combinedHills = (hillBase1 + hillBase2) / 5f; // Blend two hill patterns
combinedHills = MathF.Abs( combinedHills ); // Ensure positive-only values
// Add noise for natural detail
float baseNoise = OpenSimplex2S.Noise2( seed, nx * 6.0f, ny * 6.0f ) * noiseStrength;
float fineNoise = OpenSimplex2S.Noise2( seed + 1, nx * 12.0f, ny * 12.0f ) * (noiseStrength / 2);
// Combine hills and noise
float heightValue = (combinedHills * hillHeight) + minHeight + fineNoise;
// Ensure minimum base height
// Clamp the final height value
return Math.Clamp( heightValue, 0, 1 );
}
public static float Plateau(
int x,
int y,
int width,
int height,
long seed,
float minHeight,
bool warp,
float warpSize,
float warpStrength
)
{
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]
float ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]
float plateauHeight = 0.8f; // Maximum height of the plateau
float widthRatio = 0.75f; // Width of the side that does not extend to the edge
float slopeWidthRatio = 0.02f; // Width of the slope transition
float slopeNoiseStrength = 0.1f; // Strength of additional noise on slopes
float baseHeight = 0.1f; // Minimum terrain height
float noiseStrength = 0f; // Strength of noise for natural variation
float cutInOutStrength = 2f; // Strength of the cut-ins and jut-outs
float topNoiseStrength = 0f; // Reduced noise strength for the plateau to
// Apply domain warping for irregularity
if ( warp )
{
float warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;
float warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;
nx += warpX;
ny += warpY;
}
// Define the plateau boundaries
float plateauStartX = -1f; // Left edge
float plateauEndX = widthRatio * 2 - 1; // End of the one side
float plateauStartY = -1f; // Bottom edge
float plateauEndY = 1f; // Top edge (extends to the edge)
// Check if the current point is within the flat plateau region
bool isFlatPlateau = nx >= plateauStartX && nx <= plateauEndX && ny >= plateauStartY && ny <= plateauEndY;
// Flat plateau height
float heightValue = isFlatPlateau ? plateauHeight : 0f;
// Add slopes with jut-outs and cut-ins for smooth, natural drop-offs
if ( !isFlatPlateau )
{
// Add noise-based cut-ins and jut-outs
float noiseCut = OpenSimplex2S.Noise2( seed + 20, nx * 10.0f, ny * 10.0f ) * cutInOutStrength;
// Generate additional noise for slopes
float slopeNoise = OpenSimplex2S.Noise2( seed + 30, nx * 15.0f, ny * 15.0f ) * slopeNoiseStrength;
// Left slope
if ( nx < plateauStartX )
{
float slope = Math.Clamp( 1f - MathF.Abs( (nx - plateauStartX) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );
heightValue = Math.Max( heightValue, slope * plateauHeight );
}
// Right slope (for the single side ending early)
else if ( nx > plateauEndX )
{
float slope = Math.Clamp( 1f - MathF.Abs( (nx - plateauEndX) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );
heightValue = Math.Max( heightValue, slope * plateauHeight );
}
// Bottom slope
if ( ny < plateauStartY )
{
float slope = Math.Clamp( 1f - MathF.Abs( (ny - plateauStartY) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );
heightValue = Math.Max( heightValue, slope * plateauHeight );
}
// Top slope
if ( ny > plateauEndY )
{
float slope = Math.Clamp( 1f - MathF.Abs( (ny - plateauEndY) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );
heightValue = Math.Max( heightValue, slope * plateauHeight );
}
}
// Add noise for terrain variation
float baseNoise = OpenSimplex2S.Noise2( seed + 100, nx * 8.0f, ny * 8.0f ) * topNoiseStrength;
float fineNoise = OpenSimplex2S.Noise2( seed + 1, nx * 16.0f, ny * 16.0f ) * (noiseStrength / 2);
// Add the noise and base height to the terrain
heightValue += baseNoise + fineNoise + baseHeight;
// Ensure the base terrain height does not fall below baseHeight
heightValue = Math.Max( heightValue, baseHeight );
var heightValueBase = Math.Max( heightValue, minHeight );
// Clamp the final height
return Math.Clamp( heightValueBase, 0, 1 );
}
private static float SmoothStep( float edge0, float edge1, float x )
{
x = Math.Clamp( (x - edge0) / (edge1 - edge0), 0.0f, 1.0f ); // Normalize to [0, 1]
return x * x * (3 - 2 * x); // Smoothstep formula
}
}
Editor
library
using Editor;
using Sandbox;
using System;
namespace Sturnus.TerrainGenerationTool.RiverStream;
public static class RiverStream
{
public static float[,] AddRiversAndStreams(
float[,] heightmap,
int frequency, // Number of rivers/streams
float widthScale, // Relative width of rivers/streams
long seed
)
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float[,] modifiedHeightmap = (float[,])heightmap.Clone();
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
// Generate river starting points based on frequency
for ( int i = 0; i < frequency; i++ )
{
int startX = random.Next( 0, width );
int startY = random.Next( 0, height );
// Ensure the river starts at a relatively high elevation
while ( modifiedHeightmap[startX, startY] < 0.5f )
{
startX = random.Next( 0, width );
startY = random.Next( 0, height );
}
// Trace the river path
AddRiverPath( modifiedHeightmap, startX, startY, width, height, widthScale, random );
}
return modifiedHeightmap;
}
private static void AddRiverPath(
float[,] heightmap,
int startX,
int startY,
int width,
int height,
float widthScale,
Random random
)
{
int currentX = startX;
int currentY = startY;
// Determine the river width based on widthScale
int riverWidth = Math.Max( 1, (int)(widthScale * width) );
for ( int steps = 0; steps < width * 2; steps++ ) // Ensure rivers stretch long distances
{
// Lower the terrain at the current position to form a river bed
CarveRiverAtPosition( heightmap, currentX, currentY, riverWidth, width, height );
// Find the next position by prioritizing downhill movement
(int nextX, int nextY) = FindNextRiverPosition( heightmap, currentX, currentY, width, height, random );
// Stop if the river can no longer flow
if ( nextX == currentX && nextY == currentY )
break;
currentX = nextX;
currentY = nextY;
}
}
private static (int, int) FindNextRiverPosition(
float[,] heightmap,
int x,
int y,
int width,
int height,
Random random
)
{
float currentHeight = heightmap[x, y];
int nextX = x;
int nextY = y;
float lowestHeight = currentHeight;
// Check all 8 neighbors to find the steepest downhill path
for ( int offsetY = -1; offsetY <= 1; offsetY++ )
{
for ( int offsetX = -1; offsetX <= 1; offsetX++ )
{
int nx = x + offsetX;
int ny = y + offsetY;
// Skip out-of-bounds and current position
if ( nx < 0 || nx >= width || ny < 0 || ny >= height || (nx == x && ny == y) )
continue;
float neighborHeight = heightmap[nx, ny];
if ( neighborHeight < lowestHeight )
{
lowestHeight = neighborHeight;
nextX = nx;
nextY = ny;
}
}
}
// Add slight randomness to avoid perfectly straight rivers
if ( random.NextDouble() < 0.3 ) // 30% chance to adjust path
{
nextX = Math.Clamp( nextX + random.Next( -1, 2 ), 0, width - 1 );
nextY = Math.Clamp( nextY + random.Next( -1, 2 ), 0, height - 1 );
}
return (nextX, nextY);
}
private static void CarveRiverAtPosition(
float[,] heightmap,
int x,
int y,
int riverWidth,
int width,
int height
)
{
for ( int offsetY = -riverWidth / 2; offsetY <= riverWidth / 2; offsetY++ )
{
for ( int offsetX = -riverWidth / 2; offsetX <= riverWidth / 2; offsetX++ )
{
int nx = x + offsetX;
int ny = y + offsetY;
// Ensure we're within bounds
if ( nx >= 0 && nx < width && ny >= 0 && ny < height )
{
// Lower the terrain for the river bed
float distance = MathF.Sqrt( offsetX * offsetX + offsetY * offsetY );
float factor = Math.Clamp( 1.0f - (distance / (riverWidth / 2.0f)), 0.0f, 1.0f );
heightmap[nx, ny] -= factor * 0.03f; // Adjust depth for river carving
}
}
}
}
}
Debug: View Raw JSON Response
{
"TotalCount": 14,
"Files": [
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/OpenSimplex2S.cs",
"FileName": "OpenSimplex2S.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using System.Runtime.CompilerServices;\n\npublic static class OpenSimplex2S\n{\n\tprivate const long PRIME_X = 0x5205402B9270C86FL;\n\tprivate const long PRIME_Y = 0x598CD327003817B5L;\n\tprivate const long PRIME_Z = 0x5BCC226E9FA0BACBL;\n\tprivate const long PRIME_W = 0x56CC5227E58F554BL;\n\tprivate const long HASH_MULTIPLIER = 0x53A3F72DEEC546F5L;\n\tprivate const long SEED_FLIP_3D = -0x52D547B2E96ED629L;\n\n\tprivate const double ROOT2OVER2 = 0.7071067811865476;\n\tprivate const double SKEW_2D = 0.366025403784439;\n\tprivate const double UNSKEW_2D = -0.21132486540518713;\n\n\tprivate const double ROOT3OVER3 = 0.577350269189626;\n\tprivate const double FALLBACK_ROTATE3 = 2.0 / 3.0;\n\tprivate const double ROTATE3_ORTHOGONALIZER = UNSKEW_2D;\n\n\tprivate const float SKEW_4D = 0.309016994374947f;\n\tprivate const float UNSKEW_4D = -0.138196601125011f;\n\n\tprivate const int N_GRADS_2D_EXPONENT = 7;\n\tprivate const int N_GRADS_3D_EXPONENT = 8;\n\tprivate const int N_GRADS_4D_EXPONENT = 9;\n\tprivate const int N_GRADS_2D = 1 << N_GRADS_2D_EXPONENT;\n\tprivate const int N_GRADS_3D = 1 << N_GRADS_3D_EXPONENT;\n\tprivate const int N_GRADS_4D = 1 << N_GRADS_4D_EXPONENT;\n\n\tprivate const double NORMALIZER_2D = 0.05481866495625118;\n\tprivate const double NORMALIZER_3D = 0.2781926117527186;\n\tprivate const double NORMALIZER_4D = 0.11127401889945551;\n\n\tprivate const float RSQUARED_2D = 2.0f / 3.0f;\n\tprivate const float RSQUARED_3D = 3.0f / 4.0f;\n\tprivate const float RSQUARED_4D = 4.0f / 5.0f;\n\n\t/*\n * Noise Evaluators\n */\n\n\t/**\n * 2D OpenSimplex2S/SuperSimplex noise, standard lattice orientation.\n */\n\tpublic static float Noise2( long seed, double x, double y )\n\t{\n\t\t// Get points for A2* lattice\n\t\tdouble s = SKEW_2D * (x + y);\n\t\tdouble xs = x + s, ys = y + s;\n\n\t\treturn Noise2_UnskewedBase( seed, xs, ys );\n\t}\n\n\t/**\n * 2D OpenSimplex2S/SuperSimplex noise, with Y pointing down the main diagonal.\n * Might be better for a 2D sandbox style game, where Y is vertical.\n * Probably slightly less optimal for heightmaps or continent maps,\n * unless your map is centered around an equator. It's a slight\n * difference, but the option is here to make it easy.\n */\n\tpublic static float Noise2_ImproveX( long seed, double x, double y )\n\t{\n\t\t// Skew transform and rotation baked into one.\n\t\tdouble xx = x * ROOT2OVER2;\n\t\tdouble yy = y * (ROOT2OVER2 * (1 + 2 * SKEW_2D));\n\n\t\treturn Noise2_UnskewedBase( seed, yy + xx, yy - xx );\n\t}\n\n\t/**\n * 2D OpenSimplex2S/SuperSimplex noise base.\n */\n\tprivate static float Noise2_UnskewedBase( long seed, double xs, double ys )\n\t{\n\t\t// Get base points and offsets.\n\t\tint xsb = FastFloor( xs ), ysb = FastFloor( ys );\n\t\tfloat xi = (float)(xs - xsb), yi = (float)(ys - ysb);\n\n\t\t// Prime pre-multiplication for hash.\n\t\tlong xsbp = xsb * PRIME_X, ysbp = ysb * PRIME_Y;\n\n\t\t// Unskew.\n\t\tfloat t = (xi + yi) * (float)UNSKEW_2D;\n\t\tfloat dx0 = xi + t, dy0 = yi + t;\n\n\t\t// First vertex.\n\t\tfloat a0 = RSQUARED_2D - dx0 * dx0 - dy0 * dy0;\n\t\tfloat value = (a0 * a0) * (a0 * a0) * Grad( seed, xsbp, ysbp, dx0, dy0 );\n\n\t\t// Second vertex.\n\t\tfloat a1 = (float)(2 * (1 + 2 * UNSKEW_2D) * (1 / UNSKEW_2D + 2)) * t + ((float)(-2 * (1 + 2 * UNSKEW_2D) * (1 + 2 * UNSKEW_2D)) + a0);\n\t\tfloat dx1 = dx0 - (float)(1 + 2 * UNSKEW_2D);\n\t\tfloat dy1 = dy0 - (float)(1 + 2 * UNSKEW_2D);\n\t\tvalue += (a1 * a1) * (a1 * a1) * Grad( seed, xsbp + PRIME_X, ysbp + PRIME_Y, dx1, dy1 );\n\n\t\t// Third and fourth vertices.\n\t\t// Nested conditionals were faster than compact bit logic/arithmetic.\n\t\tfloat xmyi = xi - yi;\n\t\tif ( t < UNSKEW_2D )\n\t\t{\n\t\t\tif ( xi + xmyi > 1 )\n\t\t\t{\n\t\t\t\tfloat dx2 = dx0 - (float)(3 * UNSKEW_2D + 2);\n\t\t\t\tfloat dy2 = dy0 - (float)(3 * UNSKEW_2D + 1);\n\t\t\t\tfloat a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;\n\t\t\t\tif ( a2 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp + (PRIME_X << 1), ysbp + PRIME_Y, dx2, dy2 );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfloat dx2 = dx0 - (float)UNSKEW_2D;\n\t\t\t\tfloat dy2 = dy0 - (float)(UNSKEW_2D + 1);\n\t\t\t\tfloat a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;\n\t\t\t\tif ( a2 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp + PRIME_Y, dx2, dy2 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( yi - xmyi > 1 )\n\t\t\t{\n\t\t\t\tfloat dx3 = dx0 - (float)(3 * UNSKEW_2D + 1);\n\t\t\t\tfloat dy3 = dy0 - (float)(3 * UNSKEW_2D + 2);\n\t\t\t\tfloat a3 = RSQUARED_2D - dx3 * dx3 - dy3 * dy3;\n\t\t\t\tif ( a3 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a3 * a3) * (a3 * a3) * Grad( seed, xsbp + PRIME_X, ysbp + (PRIME_Y << 1), dx3, dy3 );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfloat dx3 = dx0 - (float)(UNSKEW_2D + 1);\n\t\t\t\tfloat dy3 = dy0 - (float)UNSKEW_2D;\n\t\t\t\tfloat a3 = RSQUARED_2D - dx3 * dx3 - dy3 * dy3;\n\t\t\t\tif ( a3 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a3 * a3) * (a3 * a3) * Grad( seed, xsbp + PRIME_X, ysbp, dx3, dy3 );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tif ( xi + xmyi < 0 )\n\t\t\t{\n\t\t\t\tfloat dx2 = dx0 + (float)(1 + UNSKEW_2D);\n\t\t\t\tfloat dy2 = dy0 + (float)UNSKEW_2D;\n\t\t\t\tfloat a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;\n\t\t\t\tif ( a2 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp - PRIME_X, ysbp, dx2, dy2 );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfloat dx2 = dx0 - (float)(UNSKEW_2D + 1);\n\t\t\t\tfloat dy2 = dy0 - (float)UNSKEW_2D;\n\t\t\t\tfloat a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;\n\t\t\t\tif ( a2 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp + PRIME_X, ysbp, dx2, dy2 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( yi < xmyi )\n\t\t\t{\n\t\t\t\tfloat dx2 = dx0 + (float)UNSKEW_2D;\n\t\t\t\tfloat dy2 = dy0 + (float)(UNSKEW_2D + 1);\n\t\t\t\tfloat a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;\n\t\t\t\tif ( a2 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp - PRIME_Y, dx2, dy2 );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfloat dx2 = dx0 - (float)UNSKEW_2D;\n\t\t\t\tfloat dy2 = dy0 - (float)(UNSKEW_2D + 1);\n\t\t\t\tfloat a2 = RSQUARED_2D - dx2 * dx2 - dy2 * dy2;\n\t\t\t\tif ( a2 > 0 )\n\t\t\t\t{\n\t\t\t\t\tvalue += (a2 * a2) * (a2 * a2) * Grad( seed, xsbp, ysbp + PRIME_Y, dx2, dy2 );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t}\n\n\t/**\n * 3D OpenSimplex2S/SuperSimplex noise, with better visual isotropy in (X, Y).\n * Recommended for 3D terrain and time-varied animations.\n * The Z coordinate should always be the \"different\" coordinate in whatever your use case is.\n * If Y is vertical in world coordinates, call Noise3_ImproveXZ(x, z, Y) or use Noise3_XZBeforeY.\n * If Z is vertical in world coordinates, call Noise3_ImproveXZ(x, y, Z).\n * For a time varied animation, call Noise3_ImproveXY(x, y, T).\n */\n\tpublic static float Noise3_ImproveXY( long seed, double x, double y, double z )\n\t{\n\t\t// Re-orient the cubic lattices without skewing, so Z points up the main lattice diagonal,\n\t\t// and the planes formed by XY are moved far out of alignment with the cube faces.\n\t\t// Orthonormal rotation. Not a skew transform.\n\t\tdouble xy = x + y;\n\t\tdouble s2 = xy * ROTATE3_ORTHOGONALIZER;\n\t\tdouble zz = z * ROOT3OVER3;\n\t\tdouble xr = x + s2 + zz;\n\t\tdouble yr = y + s2 + zz;\n\t\tdouble zr = xy * -ROOT3OVER3 + zz;\n\n\t\t// Evaluate both lattices to form a BCC lattice.\n\t\treturn Noise3_UnrotatedBase( seed, xr, yr, zr );\n\t}\n\n\t/**\n * 3D OpenSimplex2S/SuperSimplex noise, with better visual isotropy in (X, Z).\n * Recommended for 3D terrain and time-varied animations.\n * The Y coordinate should always be the \"different\" coordinate in whatever your use case is.\n * If Y is vertical in world coordinates, call Noise3_ImproveXZ(x, Y, z).\n * If Z is vertical in world coordinates, call Noise3_ImproveXZ(x, Z, y) or use Noise3_ImproveXY.\n * For a time varied animation, call Noise3_ImproveXZ(x, T, y) or use Noise3_ImproveXY.\n */\n\tpublic static float Noise3_ImproveXZ( long seed, double x, double y, double z )\n\t{\n\t\t// Re-orient the cubic lattices without skewing, so Y points up the main lattice diagonal,\n\t\t// and the planes formed by XZ are moved far out of alignment with the cube faces.\n\t\t// Orthonormal rotation. Not a skew transform.\n\t\tdouble xz = x + z;\n\t\tdouble s2 = xz * -0.211324865405187;\n\t\tdouble yy = y * ROOT3OVER3;\n\t\tdouble xr = x + s2 + yy;\n\t\tdouble zr = z + s2 + yy;\n\t\tdouble yr = xz * -ROOT3OVER3 + yy;\n\n\t\t// Evaluate both lattices to form a BCC lattice.\n\t\treturn Noise3_UnrotatedBase( seed, xr, yr, zr );\n\t}\n\n\t/**\n * 3D OpenSimplex2S/SuperSimplex noise, fallback rotation option\n * Use Noise3_ImproveXY or Noise3_ImproveXZ instead, wherever appropriate.\n * They have less diagonal bias. This function's best use is as a fallback.\n */\n\tpublic static float Noise3_Fallback( long seed, double x, double y, double z )\n\t{\n\t\t// Re-orient the cubic lattices via rotation, to produce a familiar look.\n\t\t// Orthonormal rotation. Not a skew transform.\n\t\tdouble r = FALLBACK_ROTATE3 * (x + y + z);\n\t\tdouble xr = r - x, yr = r - y, zr = r - z;\n\n\t\t// Evaluate both lattices to form a BCC lattice.\n\t\treturn Noise3_UnrotatedBase( seed, xr, yr, zr );\n\t}\n\n\t/**\n * Generate overlapping cubic lattices for 3D Re-oriented BCC noise.\n * Lookup table implementation inspired by DigitalShadow.\n * It was actually faster to narrow down the points in the loop itself,\n * than to build up the index with enough info to isolate 8 points.\n */\n\tprivate static float Noise3_UnrotatedBase( long seed, double xr, double yr, double zr )\n\t{\n\t\t// Get base points and offsets.\n\t\tint xrb = FastFloor( xr ), yrb = FastFloor( yr ), zrb = FastFloor( zr );\n\t\tfloat xi = (float)(xr - xrb), yi = (float)(yr - yrb), zi = (float)(zr - zrb);\n\n\t\t// Prime pre-multiplication for hash. Also flip seed for second lattice copy.\n\t\tlong xrbp = xrb * PRIME_X, yrbp = yrb * PRIME_Y, zrbp = zrb * PRIME_Z;\n\t\tlong seed2 = seed ^ -0x52D547B2E96ED629L;\n\n\t\t// -1 if positive, 0 if negative.\n\t\tint xNMask = (int)(-0.5f - xi), yNMask = (int)(-0.5f - yi), zNMask = (int)(-0.5f - zi);\n\n\t\t// First vertex.\n\t\tfloat x0 = xi + xNMask;\n\t\tfloat y0 = yi + yNMask;\n\t\tfloat z0 = zi + zNMask;\n\t\tfloat a0 = RSQUARED_3D - x0 * x0 - y0 * y0 - z0 * z0;\n\t\tfloat value = (a0 * a0) * (a0 * a0) * Grad( seed,\n\t\t\txrbp + (xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x0, y0, z0 );\n\n\t\t// Second vertex.\n\t\tfloat x1 = xi - 0.5f;\n\t\tfloat y1 = yi - 0.5f;\n\t\tfloat z1 = zi - 0.5f;\n\t\tfloat a1 = RSQUARED_3D - x1 * x1 - y1 * y1 - z1 * z1;\n\t\tvalue += (a1 * a1) * (a1 * a1) * Grad( seed2,\n\t\t\txrbp + PRIME_X, yrbp + PRIME_Y, zrbp + PRIME_Z, x1, y1, z1 );\n\n\t\t// Shortcuts for building the remaining falloffs.\n\t\t// Derived by subtracting the polynomials with the offsets plugged in.\n\t\tfloat xAFlipMask0 = ((xNMask | 1) << 1) * x1;\n\t\tfloat yAFlipMask0 = ((yNMask | 1) << 1) * y1;\n\t\tfloat zAFlipMask0 = ((zNMask | 1) << 1) * z1;\n\t\tfloat xAFlipMask1 = (-2 - (xNMask << 2)) * x1 - 1.0f;\n\t\tfloat yAFlipMask1 = (-2 - (yNMask << 2)) * y1 - 1.0f;\n\t\tfloat zAFlipMask1 = (-2 - (zNMask << 2)) * z1 - 1.0f;\n\n\t\tbool skip5 = false;\n\t\tfloat a2 = xAFlipMask0 + a0;\n\t\tif ( a2 > 0 )\n\t\t{\n\t\t\tfloat x2 = x0 - (xNMask | 1);\n\t\t\tfloat y2 = y0;\n\t\t\tfloat z2 = z0;\n\t\t\tvalue += (a2 * a2) * (a2 * a2) * Grad( seed,\n\t\t\t\txrbp + (~xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x2, y2, z2 );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tfloat a3 = yAFlipMask0 + zAFlipMask0 + a0;\n\t\t\tif ( a3 > 0 )\n\t\t\t{\n\t\t\t\tfloat x3 = x0;\n\t\t\t\tfloat y3 = y0 - (yNMask | 1);\n\t\t\t\tfloat z3 = z0 - (zNMask | 1);\n\t\t\t\tvalue += (a3 * a3) * (a3 * a3) * Grad( seed,\n\t\t\t\t\txrbp + (xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), x3, y3, z3 );\n\t\t\t}\n\n\t\t\tfloat a4 = xAFlipMask1 + a1;\n\t\t\tif ( a4 > 0 )\n\t\t\t{\n\t\t\t\tfloat x4 = (xNMask | 1) + x1;\n\t\t\t\tfloat y4 = y1;\n\t\t\t\tfloat z4 = z1;\n\t\t\t\tvalue += (a4 * a4) * (a4 * a4) * Grad( seed2,\n\t\t\t\t\txrbp + (xNMask & unchecked(PRIME_X * 2)), yrbp + PRIME_Y, zrbp + PRIME_Z, x4, y4, z4 );\n\t\t\t\tskip5 = true;\n\t\t\t}\n\t\t}\n\n\t\tbool skip9 = false;\n\t\tfloat a6 = yAFlipMask0 + a0;\n\t\tif ( a6 > 0 )\n\t\t{\n\t\t\tfloat x6 = x0;\n\t\t\tfloat y6 = y0 - (yNMask | 1);\n\t\t\tfloat z6 = z0;\n\t\t\tvalue += (a6 * a6) * (a6 * a6) * Grad( seed,\n\t\t\t\txrbp + (xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), x6, y6, z6 );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tfloat a7 = xAFlipMask0 + zAFlipMask0 + a0;\n\t\t\tif ( a7 > 0 )\n\t\t\t{\n\t\t\t\tfloat x7 = x0 - (xNMask | 1);\n\t\t\t\tfloat y7 = y0;\n\t\t\t\tfloat z7 = z0 - (zNMask | 1);\n\t\t\t\tvalue += (a7 * a7) * (a7 * a7) * Grad( seed,\n\t\t\t\t\txrbp + (~xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), x7, y7, z7 );\n\t\t\t}\n\n\t\t\tfloat a8 = yAFlipMask1 + a1;\n\t\t\tif ( a8 > 0 )\n\t\t\t{\n\t\t\t\tfloat x8 = x1;\n\t\t\t\tfloat y8 = (yNMask | 1) + y1;\n\t\t\t\tfloat z8 = z1;\n\t\t\t\tvalue += (a8 * a8) * (a8 * a8) * Grad( seed2,\n\t\t\t\t\txrbp + PRIME_X, yrbp + (yNMask & (PRIME_Y << 1)), zrbp + PRIME_Z, x8, y8, z8 );\n\t\t\t\tskip9 = true;\n\t\t\t}\n\t\t}\n\n\t\tbool skipD = false;\n\t\tfloat aA = zAFlipMask0 + a0;\n\t\tif ( aA > 0 )\n\t\t{\n\t\t\tfloat xA = x0;\n\t\t\tfloat yA = y0;\n\t\t\tfloat zA = z0 - (zNMask | 1);\n\t\t\tvalue += (aA * aA) * (aA * aA) * Grad( seed,\n\t\t\t\txrbp + (xNMask & PRIME_X), yrbp + (yNMask & PRIME_Y), zrbp + (~zNMask & PRIME_Z), xA, yA, zA );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tfloat aB = xAFlipMask0 + yAFlipMask0 + a0;\n\t\t\tif ( aB > 0 )\n\t\t\t{\n\t\t\t\tfloat xB = x0 - (xNMask | 1);\n\t\t\t\tfloat yB = y0 - (yNMask | 1);\n\t\t\t\tfloat zB = z0;\n\t\t\t\tvalue += (aB * aB) * (aB * aB) * Grad( seed,\n\t\t\t\t\txrbp + (~xNMask & PRIME_X), yrbp + (~yNMask & PRIME_Y), zrbp + (zNMask & PRIME_Z), xB, yB, zB );\n\t\t\t}\n\n\t\t\tfloat aC = zAFlipMask1 + a1;\n\t\t\tif ( aC > 0 )\n\t\t\t{\n\t\t\t\tfloat xC = x1;\n\t\t\t\tfloat yC = y1;\n\t\t\t\tfloat zC = (zNMask | 1) + z1;\n\t\t\t\tvalue += (aC * aC) * (aC * aC) * Grad( seed2,\n\t\t\t\t\txrbp + PRIME_X, yrbp + PRIME_Y, zrbp + (zNMask & (PRIME_Z << 1)), xC, yC, zC );\n\t\t\t\tskipD = true;\n\t\t\t}\n\t\t}\n\n\t\tif ( !skip5 )\n\t\t{\n\t\t\tfloat a5 = yAFlipMask1 + zAFlipMask1 + a1;\n\t\t\tif ( a5 > 0 )\n\t\t\t{\n\t\t\t\tfloat x5 = x1;\n\t\t\t\tfloat y5 = (yNMask | 1) + y1;\n\t\t\t\tfloat z5 = (zNMask | 1) + z1;\n\t\t\t\tvalue += (a5 * a5) * (a5 * a5) * Grad( seed2,\n\t\t\t\t\txrbp + PRIME_X, yrbp + (yNMask & (PRIME_Y << 1)), zrbp + (zNMask & (PRIME_Z << 1)), x5, y5, z5 );\n\t\t\t}\n\t\t}\n\n\t\tif ( !skip9 )\n\t\t{\n\t\t\tfloat a9 = xAFlipMask1 + zAFlipMask1 + a1;\n\t\t\tif ( a9 > 0 )\n\t\t\t{\n\t\t\t\tfloat x9 = (xNMask | 1) + x1;\n\t\t\t\tfloat y9 = y1;\n\t\t\t\tfloat z9 = (zNMask | 1) + z1;\n\t\t\t\tvalue += (a9 * a9) * (a9 * a9) * Grad( seed2,\n\t\t\t\t\txrbp + (xNMask & unchecked(PRIME_X * 2)), yrbp + PRIME_Y, zrbp + (zNMask & (PRIME_Z << 1)), x9, y9, z9 );\n\t\t\t}\n\t\t}\n\n\t\tif ( !skipD )\n\t\t{\n\t\t\tfloat aD = xAFlipMask1 + yAFlipMask1 + a1;\n\t\t\tif ( aD > 0 )\n\t\t\t{\n\t\t\t\tfloat xD = (xNMask | 1) + x1;\n\t\t\t\tfloat yD = (yNMask | 1) + y1;\n\t\t\t\tfloat zD = z1;\n\t\t\t\tvalue += (aD * aD) * (aD * aD) * Grad( seed2,\n\t\t\t\t\txrbp + (xNMask & (PRIME_X << 1)), yrbp + (yNMask & (PRIME_Y << 1)), zrbp + PRIME_Z, xD, yD, zD );\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t}\n\n\t/**\n * 4D SuperSimplex noise, with XYZ oriented like Noise3_ImproveXY\n * and W for an extra degree of freedom. W repeats eventually.\n * Recommended for time-varied animations which texture a 3D object (W=time)\n * in a space where Z is vertical\n */\n\tpublic static float Noise4_ImproveXYZ_ImproveXY( long seed, double x, double y, double z, double w )\n\t{\n\t\tdouble xy = x + y;\n\t\tdouble s2 = xy * -0.21132486540518699998;\n\t\tdouble zz = z * 0.28867513459481294226;\n\t\tdouble ww = w * 1.118033988749894;\n\t\tdouble xr = x + (zz + ww + s2), yr = y + (zz + ww + s2);\n\t\tdouble zr = xy * -0.57735026918962599998 + (zz + ww);\n\t\tdouble wr = z * -0.866025403784439 + ww;\n\n\t\treturn Noise4_UnskewedBase( seed, xr, yr, zr, wr );\n\t}\n\n\t/**\n * 4D SuperSimplex noise, with XYZ oriented like Noise3_ImproveXZ\n * and W for an extra degree of freedom. W repeats eventually.\n * Recommended for time-varied animations which texture a 3D object (W=time)\n * in a space where Y is vertical\n */\n\tpublic static float Noise4_ImproveXYZ_ImproveXZ( long seed, double x, double y, double z, double w )\n\t{\n\t\tdouble xz = x + z;\n\t\tdouble s2 = xz * -0.21132486540518699998;\n\t\tdouble yy = y * 0.28867513459481294226;\n\t\tdouble ww = w * 1.118033988749894;\n\t\tdouble xr = x + (yy + ww + s2), zr = z + (yy + ww + s2);\n\t\tdouble yr = xz * -0.57735026918962599998 + (yy + ww);\n\t\tdouble wr = y * -0.866025403784439 + ww;\n\n\t\treturn Noise4_UnskewedBase( seed, xr, yr, zr, wr );\n\t}\n\n\t/**\n * 4D SuperSimplex noise, with XYZ oriented like Noise3_Fallback\n * and W for an extra degree of freedom. W repeats eventually.\n * Recommended for time-varied animations which texture a 3D object (W=time)\n * where there isn't a clear distinction between horizontal and vertical\n */\n\tpublic static float Noise4_ImproveXYZ( long seed, double x, double y, double z, double w )\n\t{\n\t\tdouble xyz = x + y + z;\n\t\tdouble ww = w * 1.118033988749894;\n\t\tdouble s2 = xyz * -0.16666666666666666 + ww;\n\t\tdouble xs = x + s2, ys = y + s2, zs = z + s2, ws = -0.5 * xyz + ww;\n\n\t\treturn Noise4_UnskewedBase( seed, xs, ys, zs, ws );\n\t}\n\n\t/**\n * 4D SuperSimplex noise, fallback lattice orientation.\n */\n\tpublic static float Noise4_Fallback( long seed, double x, double y, double z, double w )\n\t{\n\t\t// Get points for A4 lattice\n\t\tdouble s = SKEW_4D * (x + y + z + w);\n\t\tdouble xs = x + s, ys = y + s, zs = z + s, ws = w + s;\n\n\t\treturn Noise4_UnskewedBase( seed, xs, ys, zs, ws );\n\t}\n\n\t/**\n * 4D SuperSimplex noise base.\n * Using ultra-simple 4x4x4x4 lookup partitioning.\n * This isn't as elegant or SIMD/GPU/etc. portable as other approaches,\n * but it competes performance-wise with optimized 2014 OpenSimplex.\n */\n\tprivate static float Noise4_UnskewedBase( long seed, double xs, double ys, double zs, double ws )\n\t{\n\t\t// Get base points and offsets\n\t\tint xsb = FastFloor( xs ), ysb = FastFloor( ys ), zsb = FastFloor( zs ), wsb = FastFloor( ws );\n\t\tfloat xsi = (float)(xs - xsb), ysi = (float)(ys - ysb), zsi = (float)(zs - zsb), wsi = (float)(ws - wsb);\n\n\t\t// Unskewed offsets\n\t\tfloat ssi = (xsi + ysi + zsi + wsi) * UNSKEW_4D;\n\t\tfloat xi = xsi + ssi, yi = ysi + ssi, zi = zsi + ssi, wi = wsi + ssi;\n\n\t\t// Prime pre-multiplication for hash.\n\t\tlong xsvp = xsb * PRIME_X, ysvp = ysb * PRIME_Y, zsvp = zsb * PRIME_Z, wsvp = wsb * PRIME_W;\n\n\t\t// Index into initial table.\n\t\tint index = ((FastFloor( xs * 4 ) & 3) << 0)\n\t\t\t| ((FastFloor( ys * 4 ) & 3) << 2)\n\t\t\t| ((FastFloor( zs * 4 ) & 3) << 4)\n\t\t\t| ((FastFloor( ws * 4 ) & 3) << 6);\n\n\t\t// Point contributions\n\t\tfloat value = 0;\n\t\t(int secondaryIndexStart, int secondaryIndexStop) = LOOKUP_4D_A[index];\n\t\tfor ( int i = secondaryIndexStart; i < secondaryIndexStop; i++ )\n\t\t{\n\t\t\tLatticeVertex4D c = LOOKUP_4D_B[i];\n\t\t\tfloat dx = xi + c.dx, dy = yi + c.dy, dz = zi + c.dz, dw = wi + c.dw;\n\t\t\tfloat a = (dx * dx + dy * dy) + (dz * dz + dw * dw);\n\t\t\tif ( a < RSQUARED_4D )\n\t\t\t{\n\t\t\t\ta -= RSQUARED_4D;\n\t\t\t\ta *= a;\n\t\t\t\tvalue += a * a * Grad( seed, xsvp + c.xsvp, ysvp + c.ysvp, zsvp + c.zsvp, wsvp + c.wsvp, dx, dy, dz, dw );\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/*\n * Utility\n */\n\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\n\tprivate static float Grad( long seed, long xsvp, long ysvp, float dx, float dy )\n\t{\n\t\tlong hash = seed ^ xsvp ^ ysvp;\n\t\thash *= HASH_MULTIPLIER;\n\t\thash ^= hash >> (64 - N_GRADS_2D_EXPONENT + 1);\n\t\tint gi = (int)hash & ((N_GRADS_2D - 1) << 1);\n\t\treturn GRADIENTS_2D[gi | 0] * dx + GRADIENTS_2D[gi | 1] * dy;\n\t}\n\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\n\tprivate static float Grad( long seed, long xrvp, long yrvp, long zrvp, float dx, float dy, float dz )\n\t{\n\t\tlong hash = (seed ^ xrvp) ^ (yrvp ^ zrvp);\n\t\thash *= HASH_MULTIPLIER;\n\t\thash ^= hash >> (64 - N_GRADS_3D_EXPONENT + 2);\n\t\tint gi = (int)hash & ((N_GRADS_3D - 1) << 2);\n\t\treturn GRADIENTS_3D[gi | 0] * dx + GRADIENTS_3D[gi | 1] * dy + GRADIENTS_3D[gi | 2] * dz;\n\t}\n\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\n\tprivate static float Grad( long seed, long xsvp, long ysvp, long zsvp, long wsvp, float dx, float dy, float dz, float dw )\n\t{\n\t\tlong hash = seed ^ (xsvp ^ ysvp) ^ (zsvp ^ wsvp);\n\t\thash *= HASH_MULTIPLIER;\n\t\thash ^= hash >> (64 - N_GRADS_4D_EXPONENT + 2);\n\t\tint gi = (int)hash & ((N_GRADS_4D - 1) << 2);\n\t\treturn (GRADIENTS_4D[gi | 0] * dx + GRADIENTS_4D[gi | 1] * dy) + (GRADIENTS_4D[gi | 2] * dz + GRADIENTS_4D[gi | 3] * dw);\n\t}\n\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\n\tprivate static int FastFloor( double x )\n\t{\n\t\tint xi = (int)x;\n\t\treturn x < xi ? xi - 1 : xi;\n\t}\n\n\t/*\n * Lookup Tables & Gradients\n */\n\n\tprivate static readonly float[] GRADIENTS_2D;\n\tprivate static readonly float[] GRADIENTS_3D;\n\tprivate static readonly float[] GRADIENTS_4D;\n\tprivate static readonly (short SecondaryIndexStart, short SecondaryIndexStop)[] LOOKUP_4D_A;\n\tprivate static readonly LatticeVertex4D[] LOOKUP_4D_B;\n\n\tstatic OpenSimplex2S()\n\t{\n\n\t\tGRADIENTS_2D = new float[N_GRADS_2D * 2];\n\t\tfloat[] grad2 = {\n\t\t\t\t 0.38268343236509f, 0.923879532511287f,\n\t\t\t\t 0.923879532511287f, 0.38268343236509f,\n\t\t\t\t 0.923879532511287f, -0.38268343236509f,\n\t\t\t\t 0.38268343236509f, -0.923879532511287f,\n\t\t\t\t-0.38268343236509f, -0.923879532511287f,\n\t\t\t\t-0.923879532511287f, -0.38268343236509f,\n\t\t\t\t-0.923879532511287f, 0.38268343236509f,\n\t\t\t\t-0.38268343236509f, 0.923879532511287f,\n //-------------------------------------//\n 0.130526192220052f, 0.99144486137381f,\n\t\t\t\t 0.608761429008721f, 0.793353340291235f,\n\t\t\t\t 0.793353340291235f, 0.608761429008721f,\n\t\t\t\t 0.99144486137381f, 0.130526192220051f,\n\t\t\t\t 0.99144486137381f, -0.130526192220051f,\n\t\t\t\t 0.793353340291235f, -0.60876142900872f,\n\t\t\t\t 0.608761429008721f, -0.793353340291235f,\n\t\t\t\t 0.130526192220052f, -0.99144486137381f,\n\t\t\t\t-0.130526192220052f, -0.99144486137381f,\n\t\t\t\t-0.608761429008721f, -0.793353340291235f,\n\t\t\t\t-0.793353340291235f, -0.608761429008721f,\n\t\t\t\t-0.99144486137381f, -0.130526192220052f,\n\t\t\t\t-0.99144486137381f, 0.130526192220051f,\n\t\t\t\t-0.793353340291235f, 0.608761429008721f,\n\t\t\t\t-0.608761429008721f, 0.793353340291235f,\n\t\t\t\t-0.130526192220052f, 0.99144486137381f,\n\t\t};\n\t\tfor ( int i = 0; i < grad2.Length; i++ )\n\t\t{\n\t\t\tgrad2[i] = (float)(grad2[i] / NORMALIZER_2D);\n\t\t}\n\t\tfor ( int i = 0, j = 0; i < GRADIENTS_2D.Length; i++, j++ )\n\t\t{\n\t\t\tif ( j == grad2.Length ) j = 0;\n\t\t\tGRADIENTS_2D[i] = grad2[j];\n\t\t}\n\n\t\tGRADIENTS_3D = new float[N_GRADS_3D * 4];\n\t\tfloat[] grad3 = {\n\t\t\t 2.22474487139f, 2.22474487139f, -1.0f, 0.0f,\n\t\t\t 2.22474487139f, 2.22474487139f, 1.0f, 0.0f,\n\t\t\t 3.0862664687972017f, 1.1721513422464978f, 0.0f, 0.0f,\n\t\t\t 1.1721513422464978f, 3.0862664687972017f, 0.0f, 0.0f,\n\t\t\t-2.22474487139f, 2.22474487139f, -1.0f, 0.0f,\n\t\t\t-2.22474487139f, 2.22474487139f, 1.0f, 0.0f,\n\t\t\t-1.1721513422464978f, 3.0862664687972017f, 0.0f, 0.0f,\n\t\t\t-3.0862664687972017f, 1.1721513422464978f, 0.0f, 0.0f,\n\t\t\t-1.0f, -2.22474487139f, -2.22474487139f, 0.0f,\n\t\t\t 1.0f, -2.22474487139f, -2.22474487139f, 0.0f,\n\t\t\t 0.0f, -3.0862664687972017f, -1.1721513422464978f, 0.0f,\n\t\t\t 0.0f, -1.1721513422464978f, -3.0862664687972017f, 0.0f,\n\t\t\t-1.0f, -2.22474487139f, 2.22474487139f, 0.0f,\n\t\t\t 1.0f, -2.22474487139f, 2.22474487139f, 0.0f,\n\t\t\t 0.0f, -1.1721513422464978f, 3.0862664687972017f, 0.0f,\n\t\t\t 0.0f, -3.0862664687972017f, 1.1721513422464978f, 0.0f,\n //--------------------------------------------------------------------//\n -2.22474487139f, -2.22474487139f, -1.0f, 0.0f,\n\t\t\t-2.22474487139f, -2.22474487139f, 1.0f, 0.0f,\n\t\t\t-3.0862664687972017f, -1.1721513422464978f, 0.0f, 0.0f,\n\t\t\t-1.1721513422464978f, -3.0862664687972017f, 0.0f, 0.0f,\n\t\t\t-2.22474487139f, -1.0f, -2.22474487139f, 0.0f,\n\t\t\t-2.22474487139f, 1.0f, -2.22474487139f, 0.0f,\n\t\t\t-1.1721513422464978f, 0.0f, -3.0862664687972017f, 0.0f,\n\t\t\t-3.0862664687972017f, 0.0f, -1.1721513422464978f, 0.0f,\n\t\t\t-2.22474487139f, -1.0f, 2.22474487139f, 0.0f,\n\t\t\t-2.22474487139f, 1.0f, 2.22474487139f, 0.0f,\n\t\t\t-3.0862664687972017f, 0.0f, 1.1721513422464978f, 0.0f,\n\t\t\t-1.1721513422464978f, 0.0f, 3.0862664687972017f, 0.0f,\n\t\t\t-1.0f, 2.22474487139f, -2.22474487139f, 0.0f,\n\t\t\t 1.0f, 2.22474487139f, -2.22474487139f, 0.0f,\n\t\t\t 0.0f, 1.1721513422464978f, -3.0862664687972017f, 0.0f,\n\t\t\t 0.0f, 3.0862664687972017f, -1.1721513422464978f, 0.0f,\n\t\t\t-1.0f, 2.22474487139f, 2.22474487139f, 0.0f,\n\t\t\t 1.0f, 2.22474487139f, 2.22474487139f, 0.0f,\n\t\t\t 0.0f, 3.0862664687972017f, 1.1721513422464978f, 0.0f,\n\t\t\t 0.0f, 1.1721513422464978f, 3.0862664687972017f, 0.0f,\n\t\t\t 2.22474487139f, -2.22474487139f, -1.0f, 0.0f,\n\t\t\t 2.22474487139f, -2.22474487139f, 1.0f, 0.0f,\n\t\t\t 1.1721513422464978f, -3.0862664687972017f, 0.0f, 0.0f,\n\t\t\t 3.0862664687972017f, -1.1721513422464978f, 0.0f, 0.0f,\n\t\t\t 2.22474487139f, -1.0f, -2.22474487139f, 0.0f,\n\t\t\t 2.22474487139f, 1.0f, -2.22474487139f, 0.0f,\n\t\t\t 3.0862664687972017f, 0.0f, -1.1721513422464978f, 0.0f,\n\t\t\t 1.1721513422464978f, 0.0f, -3.0862664687972017f, 0.0f,\n\t\t\t 2.22474487139f, -1.0f, 2.22474487139f, 0.0f,\n\t\t\t 2.22474487139f, 1.0f, 2.22474487139f, 0.0f,\n\t\t\t 1.1721513422464978f, 0.0f, 3.0862664687972017f, 0.0f,\n\t\t\t 3.0862664687972017f, 0.0f, 1.1721513422464978f, 0.0f,\n\t\t};\n\t\tfor ( int i = 0; i < grad3.Length; i++ )\n\t\t{\n\t\t\tgrad3[i] = (float)(grad3[i] / NORMALIZER_3D);\n\t\t}\n\t\tfor ( int i = 0, j = 0; i < GRADIENTS_3D.Length; i++, j++ )\n\t\t{\n\t\t\tif ( j == grad3.Length ) j = 0;\n\t\t\tGRADIENTS_3D[i] = grad3[j];\n\t\t}\n\n\t\tGRADIENTS_4D = new float[N_GRADS_4D * 4];\n\t\tfloat[] grad4 = {\n\t\t\t-0.6740059517812944f, -0.3239847771997537f, -0.3239847771997537f, 0.5794684678643381f,\n\t\t\t-0.7504883828755602f, -0.4004672082940195f, 0.15296486218853164f, 0.5029860367700724f,\n\t\t\t-0.7504883828755602f, 0.15296486218853164f, -0.4004672082940195f, 0.5029860367700724f,\n\t\t\t-0.8828161875373585f, 0.08164729285680945f, 0.08164729285680945f, 0.4553054119602712f,\n\t\t\t-0.4553054119602712f, -0.08164729285680945f, -0.08164729285680945f, 0.8828161875373585f,\n\t\t\t-0.5029860367700724f, -0.15296486218853164f, 0.4004672082940195f, 0.7504883828755602f,\n\t\t\t-0.5029860367700724f, 0.4004672082940195f, -0.15296486218853164f, 0.7504883828755602f,\n\t\t\t-0.5794684678643381f, 0.3239847771997537f, 0.3239847771997537f, 0.6740059517812944f,\n\t\t\t-0.6740059517812944f, -0.3239847771997537f, 0.5794684678643381f, -0.3239847771997537f,\n\t\t\t-0.7504883828755602f, -0.4004672082940195f, 0.5029860367700724f, 0.15296486218853164f,\n\t\t\t-0.7504883828755602f, 0.15296486218853164f, 0.5029860367700724f, -0.4004672082940195f,\n\t\t\t-0.8828161875373585f, 0.08164729285680945f, 0.4553054119602712f, 0.08164729285680945f,\n\t\t\t-0.4553054119602712f, -0.08164729285680945f, 0.8828161875373585f, -0.08164729285680945f,\n\t\t\t-0.5029860367700724f, -0.15296486218853164f, 0.7504883828755602f, 0.4004672082940195f,\n\t\t\t-0.5029860367700724f, 0.4004672082940195f, 0.7504883828755602f, -0.15296486218853164f,\n\t\t\t-0.5794684678643381f, 0.3239847771997537f, 0.6740059517812944f, 0.3239847771997537f,\n\t\t\t-0.6740059517812944f, 0.5794684678643381f, -0.3239847771997537f, -0.3239847771997537f,\n\t\t\t-0.7504883828755602f, 0.5029860367700724f, -0.4004672082940195f, 0.15296486218853164f,\n\t\t\t-0.7504883828755602f, 0.5029860367700724f, 0.15296486218853164f, -0.4004672082940195f,\n\t\t\t-0.8828161875373585f, 0.4553054119602712f, 0.08164729285680945f, 0.08164729285680945f,\n\t\t\t-0.4553054119602712f, 0.8828161875373585f, -0.08164729285680945f, -0.08164729285680945f,\n\t\t\t-0.5029860367700724f, 0.7504883828755602f, -0.15296486218853164f, 0.4004672082940195f,\n\t\t\t-0.5029860367700724f, 0.7504883828755602f, 0.4004672082940195f, -0.15296486218853164f,\n\t\t\t-0.5794684678643381f, 0.6740059517812944f, 0.3239847771997537f, 0.3239847771997537f,\n\t\t\t 0.5794684678643381f, -0.6740059517812944f, -0.3239847771997537f, -0.3239847771997537f,\n\t\t\t 0.5029860367700724f, -0.7504883828755602f, -0.4004672082940195f, 0.15296486218853164f,\n\t\t\t 0.5029860367700724f, -0.7504883828755602f, 0.15296486218853164f, -0.4004672082940195f,\n\t\t\t 0.4553054119602712f, -0.8828161875373585f, 0.08164729285680945f, 0.08164729285680945f,\n\t\t\t 0.8828161875373585f, -0.4553054119602712f, -0.08164729285680945f, -0.08164729285680945f,\n\t\t\t 0.7504883828755602f, -0.5029860367700724f, -0.15296486218853164f, 0.4004672082940195f,\n\t\t\t 0.7504883828755602f, -0.5029860367700724f, 0.4004672082940195f, -0.15296486218853164f,\n\t\t\t 0.6740059517812944f, -0.5794684678643381f, 0.3239847771997537f, 0.3239847771997537f,\n //------------------------------------------------------------------------------------------//\n -0.753341017856078f, -0.37968289875261624f, -0.37968289875261624f, -0.37968289875261624f,\n\t\t\t-0.7821684431180708f, -0.4321472685365301f, -0.4321472685365301f, 0.12128480194602098f,\n\t\t\t-0.7821684431180708f, -0.4321472685365301f, 0.12128480194602098f, -0.4321472685365301f,\n\t\t\t-0.7821684431180708f, 0.12128480194602098f, -0.4321472685365301f, -0.4321472685365301f,\n\t\t\t-0.8586508742123365f, -0.508629699630796f, 0.044802370851755174f, 0.044802370851755174f,\n\t\t\t-0.8586508742123365f, 0.044802370851755174f, -0.508629699630796f, 0.044802370851755174f,\n\t\t\t-0.8586508742123365f, 0.044802370851755174f, 0.044802370851755174f, -0.508629699630796f,\n\t\t\t-0.9982828964265062f, -0.03381941603233842f, -0.03381941603233842f, -0.03381941603233842f,\n\t\t\t-0.37968289875261624f, -0.753341017856078f, -0.37968289875261624f, -0.37968289875261624f,\n\t\t\t-0.4321472685365301f, -0.7821684431180708f, -0.4321472685365301f, 0.12128480194602098f,\n\t\t\t-0.4321472685365301f, -0.7821684431180708f, 0.12128480194602098f, -0.4321472685365301f,\n\t\t\t 0.12128480194602098f, -0.7821684431180708f, -0.4321472685365301f, -0.4321472685365301f,\n\t\t\t-0.508629699630796f, -0.8586508742123365f, 0.044802370851755174f, 0.044802370851755174f,\n\t\t\t 0.044802370851755174f, -0.8586508742123365f, -0.508629699630796f, 0.044802370851755174f,\n\t\t\t 0.044802370851755174f, -0.8586508742123365f, 0.044802370851755174f, -0.508629699630796f,\n\t\t\t-0.03381941603233842f, -0.9982828964265062f, -0.03381941603233842f, -0.03381941603233842f,\n\t\t\t-0.37968289875261624f, -0.37968289875261624f, -0.753341017856078f, -0.37968289875261624f,\n\t\t\t-0.4321472685365301f, -0.4321472685365301f, -0.7821684431180708f, 0.12128480194602098f,\n\t\t\t-0.4321472685365301f, 0.12128480194602098f, -0.7821684431180708f, -0.4321472685365301f,\n\t\t\t 0.12128480194602098f, -0.4321472685365301f, -0.7821684431180708f, -0.4321472685365301f,\n\t\t\t-0.508629699630796f, 0.044802370851755174f, -0.8586508742123365f, 0.044802370851755174f,\n\t\t\t 0.044802370851755174f, -0.508629699630796f, -0.8586508742123365f, 0.044802370851755174f,\n\t\t\t 0.044802370851755174f, 0.044802370851755174f, -0.8586508742123365f, -0.508629699630796f,\n\t\t\t-0.03381941603233842f, -0.03381941603233842f, -0.9982828964265062f, -0.03381941603233842f,\n\t\t\t-0.37968289875261624f, -0.37968289875261624f, -0.37968289875261624f, -0.753341017856078f,\n\t\t\t-0.4321472685365301f, -0.4321472685365301f, 0.12128480194602098f, -0.7821684431180708f,\n\t\t\t-0.4321472685365301f, 0.12128480194602098f, -0.4321472685365301f, -0.7821684431180708f,\n\t\t\t 0.12128480194602098f, -0.4321472685365301f, -0.4321472685365301f, -0.7821684431180708f,\n\t\t\t-0.508629699630796f, 0.044802370851755174f, 0.044802370851755174f, -0.8586508742123365f,\n\t\t\t 0.044802370851755174f, -0.508629699630796f, 0.044802370851755174f, -0.8586508742123365f,\n\t\t\t 0.044802370851755174f, 0.044802370851755174f, -0.508629699630796f, -0.8586508742123365f,\n\t\t\t-0.03381941603233842f, -0.03381941603233842f, -0.03381941603233842f, -0.9982828964265062f,\n\t\t\t-0.3239847771997537f, -0.6740059517812944f, -0.3239847771997537f, 0.5794684678643381f,\n\t\t\t-0.4004672082940195f, -0.7504883828755602f, 0.15296486218853164f, 0.5029860367700724f,\n\t\t\t 0.15296486218853164f, -0.7504883828755602f, -0.4004672082940195f, 0.5029860367700724f,\n\t\t\t 0.08164729285680945f, -0.8828161875373585f, 0.08164729285680945f, 0.4553054119602712f,\n\t\t\t-0.08164729285680945f, -0.4553054119602712f, -0.08164729285680945f, 0.8828161875373585f,\n\t\t\t-0.15296486218853164f, -0.5029860367700724f, 0.4004672082940195f, 0.7504883828755602f,\n\t\t\t 0.4004672082940195f, -0.5029860367700724f, -0.15296486218853164f, 0.7504883828755602f,\n\t\t\t 0.3239847771997537f, -0.5794684678643381f, 0.3239847771997537f, 0.6740059517812944f,\n\t\t\t-0.3239847771997537f, -0.3239847771997537f, -0.6740059517812944f, 0.5794684678643381f,\n\t\t\t-0.4004672082940195f, 0.15296486218853164f, -0.7504883828755602f, 0.5029860367700724f,\n\t\t\t 0.15296486218853164f, -0.4004672082940195f, -0.7504883828755602f, 0.5029860367700724f,\n\t\t\t 0.08164729285680945f, 0.08164729285680945f, -0.8828161875373585f, 0.4553054119602712f,\n\t\t\t-0.08164729285680945f, -0.08164729285680945f, -0.4553054119602712f, 0.8828161875373585f,\n\t\t\t-0.15296486218853164f, 0.4004672082940195f, -0.5029860367700724f, 0.7504883828755602f,\n\t\t\t 0.4004672082940195f, -0.15296486218853164f, -0.5029860367700724f, 0.7504883828755602f,\n\t\t\t 0.3239847771997537f, 0.3239847771997537f, -0.5794684678643381f, 0.6740059517812944f,\n\t\t\t-0.3239847771997537f, -0.6740059517812944f, 0.5794684678643381f, -0.3239847771997537f,\n\t\t\t-0.4004672082940195f, -0.7504883828755602f, 0.5029860367700724f, 0.15296486218853164f,\n\t\t\t 0.15296486218853164f, -0.7504883828755602f, 0.5029860367700724f, -0.4004672082940195f,\n\t\t\t 0.08164729285680945f, -0.8828161875373585f, 0.4553054119602712f, 0.08164729285680945f,\n\t\t\t-0.08164729285680945f, -0.4553054119602712f, 0.8828161875373585f, -0.08164729285680945f,\n\t\t\t-0.15296486218853164f, -0.5029860367700724f, 0.7504883828755602f, 0.4004672082940195f,\n\t\t\t 0.4004672082940195f, -0.5029860367700724f, 0.7504883828755602f, -0.15296486218853164f,\n\t\t\t 0.3239847771997537f, -0.5794684678643381f, 0.6740059517812944f, 0.3239847771997537f,\n\t\t\t-0.3239847771997537f, -0.3239847771997537f, 0.5794684678643381f, -0.6740059517812944f,\n\t\t\t-0.4004672082940195f, 0.15296486218853164f, 0.5029860367700724f, -0.7504883828755602f,\n\t\t\t 0.15296486218853164f, -0.4004672082940195f, 0.5029860367700724f, -0.7504883828755602f,\n\t\t\t 0.08164729285680945f, 0.08164729285680945f, 0.4553054119602712f, -0.8828161875373585f,\n\t\t\t-0.08164729285680945f, -0.08164729285680945f, 0.8828161875373585f, -0.4553054119602712f,\n\t\t\t-0.15296486218853164f, 0.4004672082940195f, 0.7504883828755602f, -0.5029860367700724f,\n\t\t\t 0.4004672082940195f, -0.15296486218853164f, 0.7504883828755602f, -0.5029860367700724f,\n\t\t\t 0.3239847771997537f, 0.3239847771997537f, 0.6740059517812944f, -0.5794684678643381f,\n\t\t\t-0.3239847771997537f, 0.5794684678643381f, -0.6740059517812944f, -0.3239847771997537f,\n\t\t\t-0.4004672082940195f, 0.5029860367700724f, -0.7504883828755602f, 0.15296486218853164f,\n\t\t\t 0.15296486218853164f, 0.5029860367700724f, -0.7504883828755602f, -0.4004672082940195f,\n\t\t\t 0.08164729285680945f, 0.4553054119602712f, -0.8828161875373585f, 0.08164729285680945f,\n\t\t\t-0.08164729285680945f, 0.8828161875373585f, -0.4553054119602712f, -0.08164729285680945f,\n\t\t\t-0.15296486218853164f, 0.7504883828755602f, -0.5029860367700724f, 0.4004672082940195f,\n\t\t\t 0.4004672082940195f, 0.7504883828755602f, -0.5029860367700724f, -0.15296486218853164f,\n\t\t\t 0.3239847771997537f, 0.6740059517812944f, -0.5794684678643381f, 0.3239847771997537f,\n\t\t\t-0.3239847771997537f, 0.5794684678643381f, -0.3239847771997537f, -0.6740059517812944f,\n\t\t\t-0.4004672082940195f, 0.5029860367700724f, 0.15296486218853164f, -0.7504883828755602f,\n\t\t\t 0.15296486218853164f, 0.5029860367700724f, -0.4004672082940195f, -0.7504883828755602f,\n\t\t\t 0.08164729285680945f, 0.4553054119602712f, 0.08164729285680945f, -0.8828161875373585f,\n\t\t\t-0.08164729285680945f, 0.8828161875373585f, -0.08164729285680945f, -0.4553054119602712f,\n\t\t\t-0.15296486218853164f, 0.7504883828755602f, 0.4004672082940195f, -0.5029860367700724f,\n\t\t\t 0.4004672082940195f, 0.7504883828755602f, -0.15296486218853164f, -0.5029860367700724f,\n\t\t\t 0.3239847771997537f, 0.6740059517812944f, 0.3239847771997537f, -0.5794684678643381f,\n\t\t\t 0.5794684678643381f, -0.3239847771997537f, -0.6740059517812944f, -0.3239847771997537f,\n\t\t\t 0.5029860367700724f, -0.4004672082940195f, -0.7504883828755602f, 0.15296486218853164f,\n\t\t\t 0.5029860367700724f, 0.15296486218853164f, -0.7504883828755602f, -0.4004672082940195f,\n\t\t\t 0.4553054119602712f, 0.08164729285680945f, -0.8828161875373585f, 0.08164729285680945f,\n\t\t\t 0.8828161875373585f, -0.08164729285680945f, -0.4553054119602712f, -0.08164729285680945f,\n\t\t\t 0.7504883828755602f, -0.15296486218853164f, -0.5029860367700724f, 0.4004672082940195f,\n\t\t\t 0.7504883828755602f, 0.4004672082940195f, -0.5029860367700724f, -0.15296486218853164f,\n\t\t\t 0.6740059517812944f, 0.3239847771997537f, -0.5794684678643381f, 0.3239847771997537f,\n\t\t\t 0.5794684678643381f, -0.3239847771997537f, -0.3239847771997537f, -0.6740059517812944f,\n\t\t\t 0.5029860367700724f, -0.4004672082940195f, 0.15296486218853164f, -0.7504883828755602f,\n\t\t\t 0.5029860367700724f, 0.15296486218853164f, -0.4004672082940195f, -0.7504883828755602f,\n\t\t\t 0.4553054119602712f, 0.08164729285680945f, 0.08164729285680945f, -0.8828161875373585f,\n\t\t\t 0.8828161875373585f, -0.08164729285680945f, -0.08164729285680945f, -0.4553054119602712f,\n\t\t\t 0.7504883828755602f, -0.15296486218853164f, 0.4004672082940195f, -0.5029860367700724f,\n\t\t\t 0.7504883828755602f, 0.4004672082940195f, -0.15296486218853164f, -0.5029860367700724f,\n\t\t\t 0.6740059517812944f, 0.3239847771997537f, 0.3239847771997537f, -0.5794684678643381f,\n\t\t\t 0.03381941603233842f, 0.03381941603233842f, 0.03381941603233842f, 0.9982828964265062f,\n\t\t\t-0.044802370851755174f, -0.044802370851755174f, 0.508629699630796f, 0.8586508742123365f,\n\t\t\t-0.044802370851755174f, 0.508629699630796f, -0.044802370851755174f, 0.8586508742123365f,\n\t\t\t-0.12128480194602098f, 0.4321472685365301f, 0.4321472685365301f, 0.7821684431180708f,\n\t\t\t 0.508629699630796f, -0.044802370851755174f, -0.044802370851755174f, 0.8586508742123365f,\n\t\t\t 0.4321472685365301f, -0.12128480194602098f, 0.4321472685365301f, 0.7821684431180708f,\n\t\t\t 0.4321472685365301f, 0.4321472685365301f, -0.12128480194602098f, 0.7821684431180708f,\n\t\t\t 0.37968289875261624f, 0.37968289875261624f, 0.37968289875261624f, 0.753341017856078f,\n\t\t\t 0.03381941603233842f, 0.03381941603233842f, 0.9982828964265062f, 0.03381941603233842f,\n\t\t\t-0.044802370851755174f, 0.044802370851755174f, 0.8586508742123365f, 0.508629699630796f,\n\t\t\t-0.044802370851755174f, 0.508629699630796f, 0.8586508742123365f, -0.044802370851755174f,\n\t\t\t-0.12128480194602098f, 0.4321472685365301f, 0.7821684431180708f, 0.4321472685365301f,\n\t\t\t 0.508629699630796f, -0.044802370851755174f, 0.8586508742123365f, -0.044802370851755174f,\n\t\t\t 0.4321472685365301f, -0.12128480194602098f, 0.7821684431180708f, 0.4321472685365301f,\n\t\t\t 0.4321472685365301f, 0.4321472685365301f, 0.7821684431180708f, -0.12128480194602098f,\n\t\t\t 0.37968289875261624f, 0.37968289875261624f, 0.753341017856078f, 0.37968289875261624f,\n\t\t\t 0.03381941603233842f, 0.9982828964265062f, 0.03381941603233842f, 0.03381941603233842f,\n\t\t\t-0.044802370851755174f, 0.8586508742123365f, -0.044802370851755174f, 0.508629699630796f,\n\t\t\t-0.044802370851755174f, 0.8586508742123365f, 0.508629699630796f, -0.044802370851755174f,\n\t\t\t-0.12128480194602098f, 0.7821684431180708f, 0.4321472685365301f, 0.4321472685365301f,\n\t\t\t 0.508629699630796f, 0.8586508742123365f, -0.044802370851755174f, -0.044802370851755174f,\n\t\t\t 0.4321472685365301f, 0.7821684431180708f, -0.12128480194602098f, 0.4321472685365301f,\n\t\t\t 0.4321472685365301f, 0.7821684431180708f, 0.4321472685365301f, -0.12128480194602098f,\n\t\t\t 0.37968289875261624f, 0.753341017856078f, 0.37968289875261624f, 0.37968289875261624f,\n\t\t\t 0.9982828964265062f, 0.03381941603233842f, 0.03381941603233842f, 0.03381941603233842f,\n\t\t\t 0.8586508742123365f, -0.044802370851755174f, -0.044802370851755174f, 0.508629699630796f,\n\t\t\t 0.8586508742123365f, -0.044802370851755174f, 0.508629699630796f, -0.044802370851755174f,\n\t\t\t 0.7821684431180708f, -0.12128480194602098f, 0.4321472685365301f, 0.4321472685365301f,\n\t\t\t 0.8586508742123365f, 0.508629699630796f, -0.044802370851755174f, -0.044802370851755174f,\n\t\t\t 0.7821684431180708f, 0.4321472685365301f, -0.12128480194602098f, 0.4321472685365301f,\n\t\t\t 0.7821684431180708f, 0.4321472685365301f, 0.4321472685365301f, -0.12128480194602098f,\n\t\t\t 0.753341017856078f, 0.37968289875261624f, 0.37968289875261624f, 0.37968289875261624f,\n\t\t};\n\t\tfor ( int i = 0; i < grad4.Length; i++ )\n\t\t{\n\t\t\tgrad4[i] = (float)(grad4[i] / NORMALIZER_4D);\n\t\t}\n\t\tfor ( int i = 0, j = 0; i < GRADIENTS_4D.Length; i++, j++ )\n\t\t{\n\t\t\tif ( j == grad4.Length ) j = 0;\n\t\t\tGRADIENTS_4D[i] = grad4[j];\n\t\t}\n\n\t\tint[][] lookup4DVertexCodes = {\n\t\t\tnew int[] { 0x15, 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x01, 0x05, 0x11, 0x15, 0x41, 0x45, 0x51, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x01, 0x15, 0x16, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x15, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xAA },\n\t\t\tnew int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xAA },\n\t\t\tnew int[] { 0x05, 0x15, 0x16, 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x04, 0x05, 0x14, 0x15, 0x44, 0x45, 0x54, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xAA },\n\t\t\tnew int[] { 0x05, 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0xAA },\n\t\t\tnew int[] { 0x05, 0x15, 0x16, 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x5B, 0x6A, 0x9A, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x04, 0x15, 0x19, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x05, 0x15, 0x19, 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x05, 0x15, 0x19, 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x5E, 0x6A, 0x9A, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x05, 0x15, 0x1A, 0x45, 0x4A, 0x55, 0x56, 0x59, 0x5A, 0x5B, 0x5E, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x15, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x11, 0x15, 0x16, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x14, 0x15, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x9A, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x15, 0x16, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x6B, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xAA },\n\t\t\tnew int[] { 0x15, 0x16, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x6B, 0x9A, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x14, 0x15, 0x19, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x19, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x6E, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x19, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x6E, 0x9A, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x1A, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x6B, 0x6E, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x10, 0x11, 0x14, 0x15, 0x50, 0x51, 0x54, 0x55, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x11, 0x15, 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x11, 0x15, 0x16, 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x67, 0x6A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x15, 0x16, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x6B, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x14, 0x15, 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xAA },\n\t\t\tnew int[] { 0x15, 0x16, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x14, 0x15, 0x19, 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x6A, 0x6D, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x19, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x6E, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x19, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6E, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x6B, 0x6E, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x10, 0x15, 0x25, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x11, 0x15, 0x25, 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x11, 0x15, 0x25, 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x6A, 0x76, 0xA6, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x11, 0x15, 0x26, 0x51, 0x55, 0x56, 0x62, 0x65, 0x66, 0x67, 0x6A, 0x76, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x14, 0x15, 0x25, 0x54, 0x55, 0x59, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x25, 0x55, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x25, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA6, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x26, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x6B, 0x7A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x14, 0x15, 0x25, 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x6A, 0x79, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x25, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x25, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x7A, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0x7A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x14, 0x15, 0x29, 0x54, 0x55, 0x59, 0x65, 0x68, 0x69, 0x6A, 0x6D, 0x79, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x15, 0x29, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x6E, 0x7A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x15, 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6E, 0x7A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0x6E, 0x7A, 0xAA, 0xAB, 0xAE, 0xBA, 0xBF },\n\t\t\tnew int[] { 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x5A, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x41, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x44, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x45, 0x46, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x5A, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xAA },\n\t\t\tnew int[] { 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x9A, 0x9B, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x44, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x49, 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x99, 0x9A, 0x9E, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x4A, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x59, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x51, 0x52, 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x56, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x45, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x58, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE },\n\t\t\tnew int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x66, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA6, 0xA7, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA },\n\t\t\tnew int[] { 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA9, 0xAA, 0xAD, 0xAE },\n\t\t\tnew int[] { 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x66, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x61, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x6A, 0xA5, 0xA6, 0xAA, 0xB6, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x62, 0x65, 0x66, 0x6A, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x54, 0x55, 0x64, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA },\n\t\t\tnew int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x55, 0x56, 0x65, 0x66, 0x6A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x6A, 0xA5, 0xA9, 0xAA, 0xB9, 0xBA },\n\t\t\tnew int[] { 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x15, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x68, 0x69, 0x6A, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x55, 0x59, 0x65, 0x69, 0x6A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x15, 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0xAA, 0xAB, 0xAE, 0xBA, 0xBF },\n\t\t\tnew int[] { 0x40, 0x41, 0x44, 0x45, 0x50, 0x51, 0x54, 0x55, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x95, 0x96, 0x9A, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x41, 0x45, 0x46, 0x51, 0x52, 0x55, 0x56, 0x95, 0x96, 0x97, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x45, 0x46, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x95, 0x99, 0x9A, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xAA },\n\t\t\tnew int[] { 0x45, 0x46, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x44, 0x45, 0x49, 0x54, 0x55, 0x58, 0x59, 0x95, 0x99, 0x9A, 0x9D, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x49, 0x55, 0x59, 0x5A, 0x95, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x49, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9E, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x99, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x51, 0x52, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x51, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xEA },\n\t\t\tnew int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x54, 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x58, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE },\n\t\t\tnew int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x96, 0x99, 0x9A, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA },\n\t\t\tnew int[] { 0x51, 0x52, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x54, 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB },\n\t\t\tnew int[] { 0x54, 0x55, 0x58, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x66, 0x69, 0x6A, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xAF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x61, 0x64, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB5, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x61, 0x65, 0x66, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x61, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xB6, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x54, 0x55, 0x64, 0x65, 0x69, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA },\n\t\t\tnew int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x6A, 0x96, 0xA5, 0xA6, 0xAA, 0xAB, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x64, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xB9, 0xBA },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA },\n\t\t\tnew int[] { 0x55, 0x56, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA, 0xBB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x6A, 0x99, 0xA5, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x55, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA, 0xBE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xBA },\n\t\t\tnew int[] { 0x40, 0x45, 0x51, 0x54, 0x55, 0x85, 0x91, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x85, 0x91, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x85, 0x91, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xD6, 0xEA },\n\t\t\tnew int[] { 0x41, 0x45, 0x51, 0x55, 0x56, 0x86, 0x92, 0x95, 0x96, 0x97, 0x9A, 0xA6, 0xAA, 0xAB, 0xD6, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x85, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xDA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xDA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x86, 0x95, 0x96, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB, 0xDA, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x85, 0x94, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xD9, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x59, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xDA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x85, 0x95, 0x96, 0x99, 0x9A, 0xAA, 0xDA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0xA6, 0xAA, 0xAB, 0xDA, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x44, 0x45, 0x54, 0x55, 0x59, 0x89, 0x95, 0x98, 0x99, 0x9A, 0x9D, 0xA9, 0xAA, 0xAE, 0xD9, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x45, 0x55, 0x59, 0x89, 0x95, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE, 0xDA, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x45, 0x55, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9E, 0xA9, 0xAA, 0xAE, 0xDA, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0x9B, 0x9E, 0xAA, 0xAB, 0xAE, 0xDA, 0xEA, 0xEF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x91, 0x94, 0x95, 0x96, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x91, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xE6, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x91, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xE6, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x92, 0x95, 0x96, 0x9A, 0xA6, 0xA7, 0xAA, 0xAB, 0xE6, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x54, 0x55, 0x94, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xE9, 0xEA },\n\t\t\tnew int[] { 0x55, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x55, 0x56, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x55, 0x56, 0x95, 0x96, 0x9A, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x94, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xE9, 0xEA },\n\t\t\tnew int[] { 0x55, 0x59, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x45, 0x55, 0x56, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x95, 0x98, 0x99, 0x9A, 0xA9, 0xAA, 0xAD, 0xAE, 0xE9, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x55, 0x59, 0x95, 0x99, 0x9A, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x45, 0x55, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x95, 0x96, 0x99, 0x9A, 0xAA, 0xAB, 0xAE, 0xEA, 0xEF },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x91, 0x94, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xE5, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x65, 0x91, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xE6, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x91, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xE6, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xE6, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x54, 0x55, 0x65, 0x94, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xE9, 0xEA },\n\t\t\tnew int[] { 0x55, 0x65, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x66, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xAA, 0xAB, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x94, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xE9, 0xEA },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x65, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xEA },\n\t\t\tnew int[] { 0x55, 0x56, 0x5A, 0x66, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xEA, 0xEB },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xE9, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x69, 0x95, 0x99, 0x9A, 0xA5, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x55, 0x59, 0x5A, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xEA, 0xEE },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xEA },\n\t\t\tnew int[] { 0x50, 0x51, 0x54, 0x55, 0x65, 0x95, 0xA1, 0xA4, 0xA5, 0xA6, 0xA9, 0xAA, 0xB5, 0xBA, 0xE5, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x51, 0x55, 0x65, 0x95, 0xA1, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA, 0xE6, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x51, 0x55, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xB6, 0xBA, 0xE6, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x51, 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA7, 0xAA, 0xAB, 0xB6, 0xBA, 0xE6, 0xEA, 0xFB },\n\t\t\tnew int[] { 0x54, 0x55, 0x65, 0x95, 0xA4, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA, 0xE9, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x55, 0x65, 0x95, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x51, 0x55, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x55, 0x56, 0x65, 0x66, 0x95, 0x96, 0xA5, 0xA6, 0xAA, 0xAB, 0xBA, 0xEA, 0xFB },\n\t\t\tnew int[] { 0x54, 0x55, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xB9, 0xBA, 0xE9, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x54, 0x55, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x55, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xBA, 0xEA, 0xFA },\n\t\t\tnew int[] { 0x55, 0x56, 0x65, 0x66, 0x6A, 0x95, 0x96, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xBA, 0xEA },\n\t\t\tnew int[] { 0x54, 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAD, 0xAE, 0xB9, 0xBA, 0xE9, 0xEA, 0xFE },\n\t\t\tnew int[] { 0x55, 0x59, 0x65, 0x69, 0x95, 0x99, 0xA5, 0xA9, 0xAA, 0xAE, 0xBA, 0xEA, 0xFE },\n\t\t\tnew int[] { 0x55, 0x59, 0x65, 0x69, 0x6A, 0x95, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAE, 0xBA, 0xEA },\n\t\t\tnew int[] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA, 0xAB, 0xAE, 0xBA, 0xEA },\n\t\t};\n\t\tLatticeVertex4D[] latticeVerticesByCode = new LatticeVertex4D[256];\n\t\tfor ( int i = 0; i < 256; i++ )\n\t\t{\n\t\t\tint cx = ((i >> 0) & 3) - 1;\n\t\t\tint cy = ((i >> 2) & 3) - 1;\n\t\t\tint cz = ((i >> 4) & 3) - 1;\n\t\t\tint cw = ((i >> 6) & 3) - 1;\n\t\t\tlatticeVerticesByCode[i] = new LatticeVertex4D( cx, cy, cz, cw );\n\t\t}\n\t\tint nLatticeVerticesTotal = 0;\n\t\tfor ( int i = 0; i < 256; i++ )\n\t\t{\n\t\t\tnLatticeVerticesTotal += lookup4DVertexCodes[i].Length;\n\t\t}\n\t\tLOOKUP_4D_A = new (short SecondaryIndexStart, short SecondaryIndexStop)[256];\n\t\tLOOKUP_4D_B = new LatticeVertex4D[nLatticeVerticesTotal];\n\t\tfor ( int i = 0, j = 0; i < 256; i++ )\n\t\t{\n\t\t\tLOOKUP_4D_A[i] = ((short)j, (short)(j + lookup4DVertexCodes[i].Length));\n\t\t\tfor ( int k = 0; k < lookup4DVertexCodes[i].Length; k++ )\n\t\t\t{\n\t\t\t\tLOOKUP_4D_B[j++] = latticeVerticesByCode[lookup4DVertexCodes[i][k]];\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate class LatticeVertex4D\n\t{\n\t\tpublic readonly float dx, dy, dz, dw;\n\t\tpublic readonly long xsvp, ysvp, zsvp, wsvp;\n\t\tpublic LatticeVertex4D( int xsv, int ysv, int zsv, int wsv )\n\t\t{\n\t\t\tthis.xsvp = xsv * PRIME_X; this.ysvp = ysv * PRIME_Y;\n\t\t\tthis.zsvp = zsv * PRIME_Z; this.wsvp = wsv * PRIME_W;\n\t\t\tfloat ssv = (xsv + ysv + zsv + wsv) * UNSKEW_4D;\n\t\t\tthis.dx = -xsv - ssv;\n\t\t\tthis.dy = -ysv - ssv;\n\t\t\tthis.dz = -zsv - ssv;\n\t\t\tthis.dw = -wsv - ssv;\n\t\t}\n\t}\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainShapes/Islands.cs",
"FileName": "Islands.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using Editor;\nusing Sandbox;\nusing System;\n\nnamespace Sturnus.TerrainGenerationTool;\npublic static class Islands\n{\n\tpublic static float Default( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize = 0.1f, float warpStrength = 0.5f )\n\t{\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\t\tfloat warpX;\n\t\tfloat warpY;\n\t\tfloat warpedNx;\n\t\tfloat warpedNy;\n\t\tfloat noise;\n\t\tif ( warp )\n\t\t{\n\t\t\t// Generate warp offsets using additional noise\n\t\t\twarpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\twarpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\n\t\t\t// Apply domain warping\n\t\t\twarpedNx = nx + warpX;\n\t\t\twarpedNy = ny + warpY;\n\t\t}\n\t\telse\n\t\t{\n\t\t\twarpedNx = nx;\n\t\t\twarpedNy = ny;\n\t\t}\n\t\t\n\t\t// Radial distance from the center\n\t\tfloat distance = (float)Math.Sqrt( nx * nx + ny * ny );\n\t\tfloat falloff = 1.0f - Math.Clamp( distance, 0.25f, 1 ); // Smooth taper from center to edge\n\n\t\t// Central mountain shape (parabolic for smooth curvature)\n\t\tfloat centralMountain = (1.0f - distance * distance) * falloff;\n\n\t\t// Beach-style taper near the edges\n\t\tfloat beachStart = 0.5f; // Start of the beach region (distance normalized)\n\t\tfloat beachEnd = 0.98f; // End of the beach region (ocean level)\n\t\tfloat beachFalloff = Math.Clamp( (distance - beachStart) / (beachEnd - beachStart), 0.1f, 1 );\n\t\tfloat beachTaper = (1.0f - beachFalloff) * 0.25f; // Smooth transition to flat region\n\n\t\t// Add subtle noise for terrain variation\n\t\tif ( warp )\n\t\t{\n\t\t\tnoise = OpenSimplex2S.Noise2( seed, warpedNx * 2, warpedNy * 2 ) * 0.4f;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tnoise = OpenSimplex2S.Noise2( seed, nx * 6, ny * 6 ) * 0.05f; // Low-frequency noise\n\t\t}\n\t\t\n\t\t// Combine components: central mountain, beach taper, and noise\n\t\tfloat output = centralMountain * (1.0f - beachFalloff) + beachTaper + noise;\n\n\t\t// Combine all effects\n\t\tfloat heightValue = output;\n\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\tfloat baseline = minHeight; // Minimum height\n\t\tfloat heightValueCombined = MathF.Max( heightValue, baseline );\n\n\t\t// Clamp the final height to valid range\n\t\treturn heightValueCombined;\n\t}\n\n\tpublic static float Archipelagos( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1;\n\t\tfloat ny = (y / (float)height) * 2 - 1;\n\n\t\t// Apply domain warping\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Use noise layers to create clusters of small islands\n\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.5f, ny * 1.5f );\n\t\tfloat secondaryNoise = OpenSimplex2S.Noise2( seed + 1, nx * 3.0f, ny * 3.0f ) * 0.5f;\n\n\t\tfloat archipelagoHeight = baseNoise + secondaryNoise;\n\n\t\t// Apply radial falloff to form rounded island clusters\n\t\tfloat distance = MathF.Sqrt( nx * nx + ny * ny );\n\t\tfloat falloff = Math.Clamp( 1 - distance * 1.2f, 0, 1 );\n\t\tfloat heightValue = Math.Clamp( archipelagoHeight * falloff, 0, 1 );\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\tfloat baseline = minHeight; // Minimum height\n\t\tfloat heightValueCombined = MathF.Max( heightValue, baseline );\n\n\t\t// Clamp the final height to valid range\n\t\treturn heightValueCombined;\n\t}\n\n\tpublic static float Atoll( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\n\t\t// Apply domain warping\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 20, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 21, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Calculate distance from the center\n\t\tfloat distance = MathF.Sqrt( nx * nx + ny * ny );\n\n\t\t// Define parameters for the single ring\n\t\tfloat ringCenter = 0.6f; // Center of the ring\n\t\tfloat ringWidth = 0.1f; // Width of the ring\n\n\t\t// Create a single ring using a Gaussian-like function\n\t\tfloat ring = MathF.Exp( -MathF.Pow( (distance - ringCenter) / ringWidth, 2 ) );\n\n\t\t// Add some noise for variation\n\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.0f, ny * 1.0f ) * 0.4f;\n\n\t\t// Introduce a beach-like area (reduce noise and height for one side of the map)\n\t\tfloat beachEffect = Math.Clamp( (1 - nx) * 0.5f, 0.2f, 1.0f ); // Reduces height on one side of the map\n\t\tfloat beachNoise = OpenSimplex2S.Noise2( seed + 30, nx * 2.0f, ny * 2.0f ) * 0.2f;\n\n\t\t// Combine the ring, noise, and beach effect\n\t\tfloat heightValue = (ring + baseNoise * beachEffect + beachNoise) * beachEffect;\n\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\tfloat baseline = minHeight; // Minimum height\n\t\tfloat heightValueCombined = MathF.Max( heightValue, baseline );\n\n\t\t// Clamp the final height to valid range\n\t\treturn heightValueCombined;\n\t}\n\n\n\n\n\tpublic static float Islets( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1;\n\t\tfloat ny = (y / (float)height) * 2 - 1;\n\n\t\t// Apply domain warping\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 30, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 31, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Generate scattered small islands\n\t\tfloat scatterNoise = OpenSimplex2S.Noise2( seed, nx * 6.0f, ny * 6.0f );\n\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed + 1, nx * 3.0f, ny * 3.0f ) * 0.5f;\n\n\t\tfloat isletHeight = scatterNoise + baseNoise;\n\n\t\t// Apply distance falloff to create isolated islets\n\t\tfloat distance = MathF.Sqrt( nx * nx + ny * ny );\n\t\tfloat falloff = Math.Clamp( 1 - distance * 1.5f, 0, 1 );\n\t\tfloat heightValue = Math.Clamp( isletHeight * falloff, 0, 1 );\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\tfloat baseline = minHeight; // Minimum height\n\t\tfloat heightValueCombined = MathF.Max( heightValue, baseline );\n\n\t\t// Clamp the final height to valid range\n\t\treturn heightValueCombined;\n\t}\n\n\tpublic static float Oceanic( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize, float warpStrength )\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1;\n\t\tfloat ny = (y / (float)height) * 2 - 1;\n\n\t\t// Apply domain warping\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 40, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 41, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Generate large, continuous landmass with a few scattered features\n\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed, nx * 1.0f, ny * 1.0f );\n\t\tfloat featureNoise = OpenSimplex2S.Noise2( seed + 1, nx * 2.0f, ny * 2.0f ) * 0.5f;\n\n\t\tfloat oceanicHeight = baseNoise + featureNoise;\n\n\t\t// Apply radial falloff for a natural ocean/land mix\n\t\tfloat distance = MathF.Sqrt( nx * nx + ny * ny );\n\t\tfloat falloff = Math.Clamp( 1 - distance * 1.0f, 0, 1 );\n\n\t\tfloat heightValue = Math.Clamp( oceanicHeight * falloff, 0, 1 );\n\n\t\tfloat baseline = minHeight; // Minimum height\n\t\tfloat heightValueCombined = MathF.Max( heightValue, baseline );\n\n\t\t// Clamp the final height to valid range\n\t\treturn heightValueCombined;\n\t}\n\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainShapes/Planetary.cs",
"FileName": "Planetary.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using Editor;\nusing Sandbox;\nusing System;\nusing System.Collections.Generic;\n\nnamespace Sturnus.TerrainGenerationTool;\npublic static class Planetary\n{\n\tpublic static float Sharded(\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tlong seed,\n\t\tfloat minHeight,\n\t\tbool warp, // Apply domain warping\n\t\tfloat warpSize, // Warp scale\n\t\tfloat warpStrength // Warp strength\n\t)\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\n\t\tfloat shardHeight = 10f;\n\t\tfloat crackDepth = 1f;\n\t\tfloat noiseStrength = 0.05f;\n\t\tint cellCount = 5;\n\n\t\t// Apply domain warping for irregularity\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Generate cracks\n\t\tfloat minDist = float.MaxValue;\n\t\tfloat secondaryDist = float.MaxValue;\n\t\tfloat cellSize = 2.0f / cellCount; // Normalize the cell size\n\t\tfor ( int i = 0; i < cellCount; i++ )\n\t\t{\n\t\t\tfor ( int j = 0; j < cellCount; j++ )\n\t\t\t{\n\t\t\t\tfloat cellX = -1 + i * cellSize + OpenSimplex2S.Noise2( seed + 20, i, j ) * cellSize * 0.5f;\n\t\t\t\tfloat cellY = -1 + j * cellSize + OpenSimplex2S.Noise2( seed + 21, i, j ) * cellSize * 0.5f;\n\n\t\t\t\tfloat distance = MathF.Sqrt( (nx - cellX) * (nx - cellX) + (ny - cellY) * (ny - cellY) );\n\n\t\t\t\tif ( distance < minDist )\n\t\t\t\t{\n\t\t\t\t\tsecondaryDist = minDist;\n\t\t\t\t\tminDist = distance;\n\t\t\t\t}\n\t\t\t\telse if ( distance < secondaryDist )\n\t\t\t\t{\n\t\t\t\t\tsecondaryDist = distance;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Compute shard height based on the secondary distance\n\t\tfloat shardNoise = OpenSimplex2S.Noise2( seed + 30, nx, ny ) * noiseStrength;\n\t\tfloat heightValue = MathF.Max( secondaryDist - minDist, 0f ) * shardHeight + shardNoise;\n\n\t\t// Apply crack depth at borders between shards\n\t\tif ( secondaryDist - minDist < 0.03f ) // Control the width of cracks\n\t\t{\n\t\t\theightValue -= crackDepth;\n\t\t}\n\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\t//heightValue = MathF.Max( heightValue, minHeight );\n\n\t\t// Clamp height to avoid negative values\n\t\theightValue = Math.Clamp( heightValue, 0, 1 );\n\n\t\tfloat baseline = minHeight; // Minimum height\n\t\tfloat heightValueCombined = MathF.Max( heightValue, baseline );\n\n\t\t// Clamp the final height to valid range\n\t\treturn heightValueCombined;\n\t}\n\n\tpublic static float Craters(\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tlong seed,\n\t\tfloat minHeight,\n\t\tbool warp, // Apply domain warping for irregularity\n\t\tfloat warpSize, // Warp scale\n\t\tfloat warpStrength // Warp strength\n\t)\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\n\t\tint craterCount = 100;\n\t\tfloat minCraterSize = 0.1f;\n float maxCraterSize = 0.3f;\n float craterDepth = 0.05f;\n float rimHeight = 0.05f;\n float rimWidthRatio = 0.2f;\n float noiseStrength = 0.05f;\n float largeCraterRatio = 0.2f;\n\t\tfloat slopeFalloff = 0.9f; // Controls smoothness of ramps\n\n\n\t\t// Apply domain warping for irregularity\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Base terrain noise\n\t\tfloat baseTerrain = OpenSimplex2S.Noise2( seed + 1, nx * 2.0f, ny * 2.0f ) * noiseStrength;\n\t\tbaseTerrain = (baseTerrain + 1) * 0.5f; // Normalize to [0, 1]\n\n\t\tfloat heightValue = baseTerrain;\n\n\t\t// Iterate through craters in reverse order to overwrite previous craters\n\t\tfor ( int i = craterCount - 1; i >= 0; i-- )\n\t\t{\n\t\t\t// Randomize crater properties\n\t\t\tfloat craterX = random.Next( -100000, 100000 ) / 50000.0f;\n\t\t\tfloat craterY = random.Next( -100000, 100000 ) / 50000.0f;\n\t\t\tfloat craterRadius = (i < craterCount * largeCraterRatio)\n\t\t\t\t? random.Next( (int)(maxCraterSize * 500), (int)(maxCraterSize * 1000) ) / 1000.0f // Large craters\n\t\t\t\t: random.Next( (int)(minCraterSize * 500), (int)(minCraterSize * 1000) ) / 1000.0f; // Small craters\n\n\t\t\t// Distance from the current point to the crater center\n\t\t\tfloat distance = MathF.Sqrt( (nx - craterX) * (nx - craterX) + (ny - craterY) * (ny - craterY) );\n\n\t\t\tif ( distance < craterRadius )\n\t\t\t{\n\t\t\t\tfloat rimStart = craterRadius * (1f - rimWidthRatio);\n\t\t\t\tfloat rimEnd = craterRadius;\n\n\t\t\t\t// Inside the pit\n\t\t\t\tif ( distance < rimStart )\n\t\t\t\t{\n\t\t\t\t\tfloat pitFalloff = Math.Clamp( 1f - (distance / rimStart), 0f, 1f );\n\t\t\t\t\theightValue = baseTerrain - MathF.Pow( pitFalloff, slopeFalloff ) * craterDepth; // Smooth ramp to the center\n\t\t\t\t}\n\t\t\t\t// Raised rim\n\t\t\t\telse if ( distance >= rimStart && distance < rimEnd )\n\t\t\t\t{\n\t\t\t\t\tfloat rimFalloff = Math.Clamp( (distance - rimStart) / (rimEnd - rimStart), 0f, 1f );\n\t\t\t\t\theightValue = baseTerrain + MathF.Pow( 1f - rimFalloff, slopeFalloff ) * rimHeight; // Rounded rim\n\t\t\t\t}\n\n\t\t\t\t// Reset terrain below the rim to prevent intersecting ridges\n\t\t\t\tif ( distance >= rimEnd )\n\t\t\t\t{\n\t\t\t\t\theightValue = baseTerrain;\n\t\t\t\t}\n\n\t\t\t\t// Exit the loop once the current crater is applied\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Ensure height values are clamped to [0, 1]\n\t\theightValue = Math.Clamp( heightValue, 0, 1 );\n\n\t\treturn heightValue;\n\t}\n\n\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainBrushes/ExtendedTerrainTool.cs",
"FileName": "ExtendedTerrainTool.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using System;\nusing System.Collections.Generic;\nusing Editor;\nusing Editor.TerrainEditor;\nusing Sandbox;\n\nnamespace Sturnus.TerrainGenerationTool.EditorTools;\n\n/// <summary>\n/// A terrain editor tool that includes every stock brush from the built-in terrain tool,\n/// plus any custom brushes defined in this library. Register your own brushes by returning\n/// them from <see cref=\"GetSubtools\"/>.\n/// </summary>\n[EditorTool]\n[Title( \"Terrain Pro\" )]\n[Icon( \"landscape\" )]\n[Alias( \"tools.terrain-pro\" )]\n[Group( \"Scene\" )]\npublic class ExtendedTerrainTool : TerrainEditorTool\n{\n\tpublic override IEnumerable<EditorTool> GetSubtools()\n\t{\n\t\t// Stock brushes, minus the Hole tool (we don't want holes in Terrain Pro)\n\t\tforeach ( var tool in base.GetSubtools() )\n\t\t{\n\t\t\tif ( tool is HoleTool )\n\t\t\t\tcontinue;\n\n\t\t\tyield return tool;\n\t\t}\n\n\t\t// Custom brushes from this library\n\t\tyield return new BulgeBrushTool( this );\n\t\tyield return new CraterBrushTool( this );\n\t\tyield return new TerraceBrushTool( this );\n\t\tyield return new NoiseBrushTool( this );\n\t}\n}\n\n/// <summary>\n/// Base for CPU-side sculpt brushes. The stock tools sculpt with a GPU compute shader;\n/// these do the same on the CPU by directly editing the terrain's heightmap array, which\n/// lets us invent entirely new brush behaviour without shipping a new shader.\n/// Also keeps the painted region highlighted while the mouse is held down.\n/// </summary>\npublic abstract class CpuSculptBrushTool : BaseBrushTool\n{\n\tushort[] _strokeBefore;\n\tRectInt _strokeRegion;\n\tbool _strokeActive;\n\n\t// Brush footprints stamped so far this stroke, for the circle highlight\n\tList<(int cx, int cy, int size)> _strokeCircles;\n\n\t// One terrain-projected decal per stamped circle, like the stock brush preview\n\tList<BrushPreviewSceneObject> _highlightObjects;\n\n\t// Deferred-apply selection: per-texel max falloff weight covered by the stroke\n\tfloat[] _selectionWeights;\n\tfloat _selectionOpacity;\n\n\t/// <summary>\n\t/// When true, painting only records the brush footprint (a selection) instead of sculpting.\n\t/// The whole selection is sculpted at once in <see cref=\"ApplySelection\"/> when the mouse is\n\t/// released, so the effect lands across the entire dragged area simultaneously.\n\t/// </summary>\n\tprotected virtual bool ApplyOnRelease => false;\n\n\tpublic override bool PaintMode { get; set; } = true;\n\n\tprotected CpuSculptBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )\n\t{\n\t}\n\n\tpublic override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\n\t\t// Keep the brush circles we've painted highlighted until the mouse is released\n\t\tif ( _strokeActive )\n\t\t{\n\t\t\tvar terrain = GetSelectedComponent<Terrain>() ?? Scene.Get<Terrain>();\n\t\t\tif ( terrain.IsValid() )\n\t\t\t\tUpdateStrokeHighlight( terrain );\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Renders a translucent brush-preview decal for every circle stamped during the stroke.\n\t/// Uses the same terrain-projected <see cref=\"BrushPreviewSceneObject\"/> the stock terrain\n\t/// tool shows for its brush preview, so the highlight hugs the terrain surface.\n\t/// </summary>\n\tvoid UpdateStrokeHighlight( Terrain terrain )\n\t{\n\t\tint res = terrain.Storage.Resolution;\n\t\tif ( res <= 0 || _strokeCircles == null ) return;\n\n\t\t// Grow/shrink the decal list to match the number of stamped circles\n\t\tif ( _highlightObjects == null )\n\t\t\t_highlightObjects = new List<BrushPreviewSceneObject>();\n\n\t\twhile ( _highlightObjects.Count < _strokeCircles.Count )\n\t\t\t_highlightObjects.Add( new BrushPreviewSceneObject( Gizmo.World ) );\n\n\t\twhile ( _highlightObjects.Count > _strokeCircles.Count )\n\t\t{\n\t\t\tvar extra = _highlightObjects[^1];\n\t\t\textra.Delete();\n\t\t\t_highlightObjects.RemoveAt( _highlightObjects.Count - 1 );\n\t\t}\n\n\t\tvar tx = terrain.WorldTransform;\n\t\tfloat heightScale = terrain.Storage.TerrainHeight / 65535f;\n\t\tfloat unitsPerTexel = terrain.Storage.TerrainSize / (float)res;\n\n\t\tfor ( int i = 0; i < _strokeCircles.Count; i++ )\n\t\t{\n\t\t\tvar (cx, cy, size) = _strokeCircles[i];\n\n\t\t\tint hx = Math.Clamp( cx, 0, res - 1 );\n\t\t\tint hy = Math.Clamp( cy, 0, res - 1 );\n\t\t\tfloat h = terrain.Storage.HeightMap[hy * res + hx] * heightScale;\n\n\t\t\tvar obj = _highlightObjects[i];\n\t\t\tobj.RenderLayer = SceneRenderLayer.OverlayWithDepth;\n\t\t\tobj.Bounds = BBox.FromPositionAndSize( 0, float.MaxValue );\n\t\t\tobj.Transform = new Transform( tx.PointToWorld( new Vector3( cx * unitsPerTexel, cy * unitsPerTexel, h ) ), tx.Rotation );\n\t\t\tobj.Radius = size * 0.5f * unitsPerTexel;\n\t\t\tobj.Texture = TerrainEditorTool.Brush?.Texture;\n\t\t\tobj.Color = Color.FromBytes( 255, 165, 0 ).WithAlpha( 0.5f );\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Deletes the highlight decals, called when the stroke ends.\n\t/// </summary>\n\tvoid ClearStrokeHighlight()\n\t{\n\t\tif ( _highlightObjects != null )\n\t\t{\n\t\t\tforeach ( var obj in _highlightObjects )\n\t\t\t\tobj.Delete();\n\t\t\t_highlightObjects.Clear();\n\t\t}\n\t}\n\n\tprotected override void OnPaint( Terrain terrain, TerrainPaintParameters paint )\n\t{\n\t\tint res = terrain.Storage.Resolution;\n\n\t\t// Brush footprint in texels\n\t\tint size = (int)Math.Floor( paint.BrushSettings.Size * 2.0f / terrain.Storage.TerrainSize * res );\n\t\tsize = Math.Max( size, 1 );\n\n\t\tint cx = (int)Math.Floor( paint.HitUV.x * res );\n\t\tint cy = (int)Math.Floor( paint.HitUV.y * res );\n\n\t\tvar region = new RectInt( cx - size / 2, cy - size / 2, size + 1, size + 1 );\n\n\t\t// On the first paint of a stroke, snapshot the entire heightmap so undo can restore it\n\t\t// no matter how far the stroke drags.\n\t\tif ( !_strokeActive )\n\t\t{\n\t\t\t_strokeActive = true;\n\t\t\t_strokeRegion = region;\n\t\t\t_strokeBefore = (ushort[])terrain.Storage.HeightMap.Clone();\n\t\t\t_strokeCircles = new List<(int, int, int)>();\n\n\t\t\tif ( ApplyOnRelease )\n\t\t\t{\n\t\t\t\t_selectionWeights = new float[res * res];\n\t\t\t\t_selectionOpacity = paint.BrushSettings.Opacity;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Expand the dirty region to cover this frame's footprint\n\t\t\tint left = Math.Min( _strokeRegion.Left, region.Left );\n\t\t\tint top = Math.Min( _strokeRegion.Top, region.Top );\n\t\t\tint right = Math.Max( _strokeRegion.Right, region.Right );\n\t\t\tint bottom = Math.Max( _strokeRegion.Bottom, region.Bottom );\n\t\t\t_strokeRegion = new RectInt( left, top, right - left, bottom - top );\n\t\t}\n\n\t\t_strokeCircles.Add( (cx, cy, size) );\n\n\t\tif ( ApplyOnRelease )\n\t\t{\n\t\t\t// Only mark the selection - no height changes until the mouse is released\n\t\t\tStampSelection( terrain, paint, res, cx, cy, size );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Let the brush decide how to edit each texel\n\t\t\tSculpt( terrain, paint, res, cx, cy, size );\n\n\t\t\t// Upload CPU -> GPU and refresh collision so the sculpt shows live\n\t\t\tterrain.SyncGPUTexture();\n\t\t\tterrain.UpdateCollision( Terrain.SyncFlags.Height, _strokeRegion );\n\t\t}\n\t}\n\n\tprotected abstract void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size );\n\n\t/// <summary>\n\t/// Records the brush's falloff weight for every texel in the footprint. The strongest weight\n\t/// any stamp leaves on a texel wins, so overlapping circles blend into one clean selection.\n\t/// </summary>\n\tvoid StampSelection( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )\n\t{\n\t\tint radius = size / 2;\n\n\t\tfor ( int y = -radius; y <= radius; y++ )\n\t\t{\n\t\t\tfor ( int x = -radius; x <= radius; x++ )\n\t\t\t{\n\t\t\t\tint tx = centerX + x;\n\t\t\t\tint ty = centerY + y;\n\t\t\t\tif ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;\n\n\t\t\t\tfloat w = SampleBrush( paint, x + radius, y + radius, size );\n\t\t\t\tif ( w <= 0.001f ) continue;\n\n\t\t\t\tint index = ty * res + tx;\n\t\t\t\tif ( w > _selectionWeights[index] )\n\t\t\t\t\t_selectionWeights[index] = w;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Applies the brush over the whole recorded selection at once. Called on mouse release when\n\t/// <see cref=\"ApplyOnRelease\"/> is true. The weights array holds the max falloff weight for\n\t/// every texel touched by the stroke.\n\t/// </summary>\n\tprotected virtual void ApplySelection( Terrain terrain, int res, float[] weights, float opacity )\n\t{\n\t}\n\n\tprotected override void OnPaintEnded( Terrain terrain )\n\t{\n\t\tif ( _strokeActive )\n\t\t{\n\t\t\tint res = terrain.Storage.Resolution;\n\n\t\t\t// Clamp the unioned dirty region to the terrain bounds\n\t\t\t_strokeRegion.Left = Math.Clamp( _strokeRegion.Left, 0, res - 1 );\n\t\t\t_strokeRegion.Right = Math.Clamp( _strokeRegion.Right, 0, res - 1 );\n\t\t\t_strokeRegion.Top = Math.Clamp( _strokeRegion.Top, 0, res - 1 );\n\t\t\t_strokeRegion.Bottom = Math.Clamp( _strokeRegion.Bottom, 0, res - 1 );\n\n\t\t\tif ( ApplyOnRelease && _selectionWeights != null )\n\t\t\t{\n\t\t\t\t// Sculpt the entire dragged selection in one go, then sync once\n\t\t\t\tApplySelection( terrain, res, _selectionWeights, _selectionOpacity );\n\t\t\t\tterrain.SyncGPUTexture();\n\t\t\t\tterrain.UpdateCollision( Terrain.SyncFlags.Height, _strokeRegion );\n\t\t\t}\n\n\t\t\tushort[] after = (ushort[])terrain.Storage.HeightMap.Clone();\n\t\t\tvar region = _strokeRegion;\n\n\t\t\tAction Restore( ushort[] data ) => () =>\n\t\t\t{\n\t\t\t\tif ( !terrain.IsValid() ) return;\n\t\t\t\tWriteHeightRegion( terrain.Storage.HeightMap, res, region, data );\n\t\t\t\tterrain.SyncGPUTexture();\n\t\t\t\tterrain.UpdateCollision( Terrain.SyncFlags.Height, region );\n\t\t\t};\n\n\t\t\tSceneEditorSession.Active.UndoSystem.Insert( $\"Terrain {DisplayInfo.For( this ).Name}\", Restore( _strokeBefore ), Restore( after ) );\n\n\t\t\t_strokeBefore = null;\n\t\t\t_strokeActive = false;\n\t\t\t_strokeCircles = null;\n\t\t\t_selectionWeights = null;\n\t\t\tClearStrokeHighlight();\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Sample the selected brush's falloff at a local footprint coordinate.\n\t/// Returns 0..1, 1 in the middle, 0 at the edges.\n\t/// </summary>\n\tprotected float SampleBrush( TerrainPaintParameters paint, int localX, int localY, int size )\n\t{\n\t\tif ( size <= 0 ) return 0f;\n\n\t\tvar pixmap = paint.Brush?.Pixmap;\n\t\tif ( pixmap != null && pixmap.Width > 0 && pixmap.Height > 0 )\n\t\t{\n\t\t\tfloat u = (localX + 0.5f) / size;\n\t\t\tfloat v = (localY + 0.5f) / size;\n\n\t\t\tint px = Math.Clamp( (int)(u * pixmap.Width), 0, pixmap.Width - 1 );\n\t\t\tint py = Math.Clamp( (int)(v * pixmap.Height), 0, pixmap.Height - 1 );\n\n\t\t\tvar c = pixmap.GetPixel( px, py );\n\t\t\treturn Math.Clamp( c.r, 0f, 1f );\n\t\t}\n\n\t\t// Fallback: soft radial falloff\n\t\tfloat nx = (localX + 0.5f - size * 0.5f) / (size * 0.5f);\n\t\tfloat ny = (localY + 0.5f - size * 0.5f) / (size * 0.5f);\n\t\tfloat d = MathF.Sqrt( nx * nx + ny * ny );\n\t\treturn Math.Clamp( 1f - d, 0f, 1f );\n\t}\n\n\t/// <summary>\n\t/// Writes a full heightmap snapshot into the given dirty region. The snapshot may be a full\n\t/// map clone, but we only copy the region we actually painted so collision updates stay cheap.\n\t/// </summary>\n\tstatic void WriteHeightRegion( ushort[] heightmap, int res, RectInt region, ushort[] data )\n\t{\n\t\tfor ( int y = 0; y < region.Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < region.Width; x++ )\n\t\t\t{\n\t\t\t\theightmap[region.Left + x + (region.Top + y) * res] = data[region.Left + x + (region.Top + y) * res];\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// <summary>\n/// Raises a smooth dome inside the brush footprint.\n/// </summary>\n[Title( \"Bulge\" )]\n[Icon( \"bubble_chart\" )]\n[Alias( \"tools.terrain.bulge\" )]\n[Group( \"1\" )]\n[Order( 1 )]\npublic class BulgeBrushTool : CpuSculptBrushTool\n{\n\tpublic BulgeBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )\n\t{\n\t}\n\n\tprotected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )\n\t{\n\t\tvar heightmap = terrain.Storage.HeightMap;\n\t\tfloat opacity = paint.BrushSettings.Opacity;\n\t\tint radius = size / 2;\n\n\t\tfor ( int y = -radius; y <= radius; y++ )\n\t\t{\n\t\t\tfor ( int x = -radius; x <= radius; x++ )\n\t\t\t{\n\t\t\t\tint tx = centerX + x;\n\t\t\t\tint ty = centerY + y;\n\t\t\t\tif ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;\n\n\t\t\t\tfloat brush = SampleBrush( paint, x + radius, y + radius, size );\n\t\t\t\tif ( brush <= 0.001f ) continue;\n\n\t\t\t\tint index = ty * res + tx;\n\t\t\t\tfloat current = heightmap[index] / 65535f;\n\n\t\t\t\t// Parabolic dome: 1 at centre, 0 at the edge\n\t\t\t\tfloat dome = brush * brush;\n\t\t\t\tfloat target = current + dome * opacity;\n\n\t\t\t\theightmap[index] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// <summary>\n/// Digs a rounded depression with a slightly raised rim, like an impact crater.\n/// </summary>\n[Title( \"Crater\" )]\n[Icon( \"brightness_low\" )]\n[Alias( \"tools.terrain.crater\" )]\n[Group( \"1\" )]\n[Order( 1 )]\npublic class CraterBrushTool : CpuSculptBrushTool\n{\n\tpublic CraterBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )\n\t{\n\t}\n\n\tprotected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )\n\t{\n\t\tvar heightmap = terrain.Storage.HeightMap;\n\t\tfloat opacity = paint.BrushSettings.Opacity;\n\t\tint radius = size / 2;\n\n\t\tfor ( int y = -radius; y <= radius; y++ )\n\t\t{\n\t\t\tfor ( int x = -radius; x <= radius; x++ )\n\t\t\t{\n\t\t\t\tint tx = centerX + x;\n\t\t\t\tint ty = centerY + y;\n\t\t\t\tif ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;\n\n\t\t\t\tfloat brush = SampleBrush( paint, x + radius, y + radius, size );\n\t\t\t\tif ( brush <= 0.001f ) continue;\n\n\t\t\t\tint index = ty * res + tx;\n\t\t\t\tfloat current = heightmap[index] / 65535f;\n\n\t\t\t\t// Depression with a raised rim: dip in the middle, bump near the edge\n\t\t\t\tfloat rim = brush < 0.75f ? -brush : (brush - 0.75f) / 0.25f;\n\t\t\t\tfloat target = current + rim * opacity;\n\n\t\t\t\theightmap[index] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// <summary>\n/// Snaps heights to evenly spaced terraced steps within the brush footprint.\n/// </summary>\n[Title( \"Terrace\" )]\n[Icon( \"stairs\" )]\n[Alias( \"tools.terrain.terrace\" )]\n[Group( \"1\" )]\n[Order( 1 )]\npublic class TerraceBrushTool : CpuSculptBrushTool\n{\n\tpublic TerraceBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )\n\t{\n\t}\n\n\tprotected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )\n\t{\n\t\tvar heightmap = terrain.Storage.HeightMap;\n\t\tfloat opacity = paint.BrushSettings.Opacity;\n\t\tint radius = size / 2;\n\n\t\t// Step every 8% of full height - you could expose this as a setting later\n\t\tconst float stepSize = 0.08f;\n\n\t\tfor ( int y = -radius; y <= radius; y++ )\n\t\t{\n\t\t\tfor ( int x = -radius; x <= radius; x++ )\n\t\t\t{\n\t\t\t\tint tx = centerX + x;\n\t\t\t\tint ty = centerY + y;\n\t\t\t\tif ( tx < 0 || ty < 0 || tx >= res || ty >= res ) continue;\n\n\t\t\t\tfloat brush = SampleBrush( paint, x + radius, y + radius, size );\n\t\t\t\tif ( brush <= 0.001f ) continue;\n\n\t\t\t\tint index = ty * res + tx;\n\t\t\t\tfloat current = heightmap[index] / 65535f;\n\n\t\t\t\tfloat stepped = MathF.Round( current / stepSize ) * stepSize;\n\t\t\t\tfloat target = MathX.LerpTo( current, stepped, opacity * brush );\n\n\t\t\t\theightmap[index] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// <summary>\n/// Adds adjustable simplex noise to the terrain, driven by the library's OpenSimplex2S\n/// generator. Frequency, strength and seed can be tuned with the toolbar sliders.\n/// </summary>\n[Title( \"Noise\" )]\n[Icon( \"shuffle\" )]\n[Alias( \"tools.terrain.noise\" )]\n[Group( \"1\" )]\n[Order( 1 )]\npublic class NoiseBrushTool : CpuSculptBrushTool\n{\n\t/// <summary>Noise frequency - higher = more, smaller bumps.</summary>\n\t[Property, Range( 0.5f, 20f ), Step( 0.1f ), WideMode] public float Frequency { get; set; } = 4f;\n\n\t/// <summary>How strongly the noise displaces the height (0..1, fraction of full height).</summary>\n\t[Property, Range( 0.001f, 0.05f ), Step( 0.001f ), WideMode] public float Strength { get; set; } = 0.008f;\n\n\t/// <summary>Random seed for the noise field.</summary>\n\t[Property, Range( 0, 100000 ), Step( 1 ), WideMode] public int NoiseSeed { get; set; } = 1337;\n\n\tpublic NoiseBrushTool( TerrainEditorTool terrainEditorTool ) : base( terrainEditorTool )\n\t{\n\t}\n\n\t// Noise is applied once across the whole dragged selection when the mouse is released,\n\t// not per-frame while painting.\n\tprotected override bool ApplyOnRelease => true;\n\n\t/// <summary>\n\t/// Shows the stock terrain brush settings plus a \"Noise Settings\" group bound to the\n\t/// noise properties, so Frequency / Strength / Seed can be tuned in the sidebar.\n\t/// </summary>\n\tpublic override Widget CreateToolSidebar()\n\t{\n\t\tif ( _parent is null ) return null;\n\n\t\tvar sidebar = (ToolSidebarWidget)_parent.CreateToolSidebar();\n\t\tif ( sidebar is null ) return null;\n\n\t\tvar so = EditorTypeLibrary.GetSerializedObject( this );\n\t\tvar group = sidebar.AddGroup( \"Noise Settings\" );\n\n\t\tvar sheet = new ControlSheet();\n\t\tsheet.AddObject( so, prop =>\n\t\t\tprop.Name is nameof( Frequency ) or nameof( Strength ) or nameof( NoiseSeed ) );\n\t\tgroup.Add( sheet );\n\n\t\treturn sidebar;\n\t}\n\n\t// Not used - ApplyOnRelease routes painting through ApplySelection instead.\n\tprotected override void Sculpt( Terrain terrain, TerrainPaintParameters paint, int res, int centerX, int centerY, int size )\n\t{\n\t}\n\n\t/// <summary>\n\t/// Applies the noise field across every texel covered by the stroke, using the strongest\n\t/// brush falloff weight recorded for each texel.\n\t/// </summary>\n\tprotected override void ApplySelection( Terrain terrain, int res, float[] weights, float opacity )\n\t{\n\t\tvar heightmap = terrain.Storage.HeightMap;\n\n\t\tfor ( int i = 0; i < weights.Length; i++ )\n\t\t{\n\t\t\tfloat w = weights[i];\n\t\t\tif ( w <= 0.001f ) continue;\n\n\t\t\tint tx = i % res;\n\t\t\tint ty = i / res;\n\n\t\t\t// Sample simplex noise at the texel, in [0,1], then remap to [-1,1]\n\t\t\t// so the noise can both add and subtract height.\n\t\t\tfloat noise = OpenSimplex2S.Noise2( NoiseSeed, tx * Frequency, ty * Frequency );\n\t\t\tnoise = noise * 2f - 1f;\n\n\t\t\tfloat current = heightmap[i] / 65535f;\n\t\t\tfloat target = current + noise * Strength * opacity * w;\n\n\t\t\theightmap[i] = (ushort)Math.Clamp( target * 65535f, 0, 65535 );\n\t\t}\n\t}\n\n\tpublic override Widget CreateToolbarWidget()\n\t{\n\t\tvar group = new Widget();\n\t\tgroup.FixedHeight = Theme.RowHeight;\n\t\tgroup.Layout = Layout.Row();\n\t\tgroup.Layout.Spacing = 6;\n\n\t\tgroup.Layout.Add( new Label( \"Frequency\" ) );\n\t\tvar freq = new FloatSlider( group );\n\t\tfreq.Minimum = 0.5f;\n\t\tfreq.Maximum = 20f;\n\t\tfreq.Step = 0.1f;\n\t\tfreq.Value = Frequency;\n\t\tfreq.OnValueEdited = () => Frequency = freq.Value;\n\t\tgroup.Layout.Add( freq, 1 );\n\n\t\tgroup.Layout.Add( new Label( \"Strength\" ) );\n\t\tvar strength = new FloatSlider( group );\n\t\tstrength.Minimum = 0.001f;\n\t\tstrength.Maximum = 0.05f;\n\t\tstrength.Step = 0.001f;\n\t\tstrength.Value = Strength;\n\t\tstrength.OnValueEdited = () => Strength = strength.Value;\n\t\tgroup.Layout.Add( strength, 1 );\n\n\t\tgroup.Layout.Add( new Label( \"Seed\" ) );\n\t\tvar seed = new FloatSlider( group );\n\t\tseed.Minimum = 0;\n\t\tseed.Maximum = 100000;\n\t\tseed.Step = 1;\n\t\tseed.Value = NoiseSeed;\n\t\tseed.OnValueEdited = () => NoiseSeed = (int)seed.Value;\n\t\tgroup.Layout.Add( seed, 1 );\n\n\t\tgroup.OnPaintOverride = () =>\n\t\t{\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.ControlBackground );\n\t\t\tPaint.DrawRect( group.LocalRect, Theme.ControlRadius );\n\t\t\treturn true;\n\t\t};\n\n\t\treturn group;\n\t}\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": ".obj/__compiler_extra.cs",
"FileName": "__compiler_extra.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 339832,
"Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Terrain Pro\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"terraingenerationtool\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"sturnus\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"sturnus.terraingenerationtool\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-08-06T23:17:13.0077717Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.327.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.327.0\")]"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Code/File.cs",
"FileName": "File.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 339832,
"Code": "using Sandbox;\n\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "File.cs",
"FileName": "File.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 339832,
"Code": "using Sandbox;\n\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/Noise/Lanczos.cs",
"FileName": "Lanczos.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using System;\nusing System.Collections.Generic;\nusing System.Drawing;\nusing System.Text;\n\nnamespace Sturnus.TerrainGenerationTool.Noise.Lanczos\n{\n\t//A version of value-noise using Lanczos-Resampling. Also has classic billiniar-noise\n\t//Made by Zomare\n\n\tclass ValueNoise\n\t{\n\t\t//Hash-Function for rng, outputs in range [-1, 1]\n\t\tstatic float Random( int x1, int y1 )\n\t\t{\n\t\t\tbyte[] table = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7,\n\t\t\t\t\t\t\t225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247,\n\t\t\t\t\t\t\t120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,\n\t\t\t\t\t\t\t88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134,\n\t\t\t\t\t\t\t139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220,\n\t\t\t\t\t\t\t105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80,\n\t\t\t\t\t\t\t73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,\n\t\t\t\t\t\t\t164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38,\n\t\t\t\t\t\t\t147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189,\n\t\t\t\t\t\t\t28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101,\n\t\t\t\t\t\t\t155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232,\n\t\t\t\t\t\t\t178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12,\n\t\t\t\t\t\t\t191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181,\n\t\t\t\t\t\t\t199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236,\n\t\t\t\t\t\t\t205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180};\n\n\t\t\treturn ((float)table[(x1 + table[y1 & 255]) & 255] / 255f) * 2 - 1;\n\t\t}\n\n\n\t\tpublic static float Compute( float x, float y )\n\t\t{\n\t\t\tint ix = (int)x;\n\t\t\tint iy = (int)y;\n\n\t\t\tfloat dx = x - ix;\n\t\t\tfloat dy = y - iy;\n\n\t\t\t//averages used for normalizing\n\t\t\tfloat avgY = 0;\n\t\t\tfloat avgX = 0;\n\n\t\t\t//Calculating lookup table for faster horizontal interpolation\n\t\t\tfloat[] lanczosX = new float[6];\n\n\t\t\tfor ( int px = -2; px < 4; px++ )\n\t\t\t{\n\t\t\t\tfloat f = Lanczos( dx - px );\n\t\t\t\tavgX += f;\n\t\t\t\tlanczosX[px + 2] = f;\n\t\t\t}\n\n\t\t\tfloat n = 0;\n\n\t\t\tfor ( int py = -2; py < 4; py++ )\n\t\t\t{\n\t\t\t\tfloat a = 0;\n\n\t\t\t\tfor ( int px = -2; px < 4; px++ )\n\t\t\t\t{\n\t\t\t\t\ta += Random( ix + px, iy + py ) * lanczosX[px + 2];\n\t\t\t\t}\n\n\t\t\t\ta /= avgX;\n\t\t\t\tn += a * Lanczos( dy - py );\n\t\t\t\tavgY += Lanczos( dy - py );\n\t\t\t}\n\n\t\t\t//!Not correctly normalized!\n\t\t\treturn smoothstep( -1, 1, (n / avgY / 1.25f + 1) / 2f );\n\t\t}\n\n\t\t//Lanczos function used for interpolation\n\t\t//L(x)=sinc(x)sinc(x/a)\n\t\tstatic float Lanczos( float t )\n\t\t{\n\t\t\tif ( t == 0 )\n\t\t\t{\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t\telse if ( t > 4 || t < -4 )\n\t\t\t{\n\t\t\t\treturn 0;\n\t\t\t}\n\n\t\t\treturn 3 * (float)((Math.Sin( Math.PI * t ) * Math.Sin( Math.PI * (t / 3) )) / (Math.PI * Math.PI * t * t));\n\t\t}\n\n\n\t\t//Left in for the purpose of maybe using it later\n\t\tstatic float Sinc( float x )\n\t\t{\n\t\t\treturn (float)(Math.Sin( Math.PI * x ) / (Math.PI * x));\n\t\t}\n\n\t\t//Outputs an low frequency octave of noise as a png\n\t\t/*static public void Test( int d )\n\t\t{\n\t\t\tvar bm = new Bitmap( d, d );\n\n\t\t\tint off = new Random().Next( -1000, 1000 );\n\n\t\t\tfor ( int y = 0; y < d; y++ )\n\t\t\t{\n\t\t\t\tfor ( int x = 0; x < d; x++ )\n\t\t\t\t{\n\t\t\t\t\tint c = (int)(255 * (Compute( x * 0.025f, y * 0.025f + off ) + 1) / 2f);\n\n\t\t\t\t\tbm.SetPixel( x, y, Color.FromArgb( c, c, c ) );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbm.Save( \"test.png\" );\n\t\t}*/\n\n\t\t//Generates a worldmap and outputs it as a png\n\t\t/*static public void Generate( int d )\n\t\t{\n\t\t\tvar bm = new Bitmap( d, d );\n\n\t\t\tint off = new Random().Next( -1000, 1000 );\n\n\t\t\tfor ( int y = 0; y < d; y++ )\n\t\t\t{\n\t\t\t\tfor ( int x = 0; x < d; x++ )\n\t\t\t\t{\n\t\t\t\t\tfloat a = Math.Abs( ComputeFractal( x * 0.02f, y * 0.02f + off, 4 ) );\n\n\t\t\t\t\tif ( a > 0.2f )\n\t\t\t\t\t{\n\t\t\t\t\t\ta = 0.2f;\n\t\t\t\t\t}\n\t\t\t\t\ta /= 0.2f;\n\n\n\t\t\t\t\ta *= ComputeFractal( x * 0.025f, y * 0.025f + 1000 + off, 8 ) * 0.5f + 0.5f;\n\n\t\t\t\t\tColor c = Color.Aqua;\n\n\n\t\t\t\t\tif ( a > 0.9f )\n\t\t\t\t\t{\n\t\t\t\t\t\tc = Color.White;\n\t\t\t\t\t}\n\t\t\t\t\telse if ( a > 0.75f )\n\t\t\t\t\t{\n\t\t\t\t\t\tc = Color.LightGray;\n\t\t\t\t\t}\n\t\t\t\t\telse if ( a > 0.4f )\n\t\t\t\t\t{\n\t\t\t\t\t\tc = Color.ForestGreen;\n\t\t\t\t\t}\n\n\t\t\t\t\tbm.SetPixel( x, y, c );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbm.Save( \"map.png\" );\n\t\t}*/\n\n\t\t//Fractal Noise; n: amount of octaves\n\t\tstatic public float ComputeFractal( float x, float y, int n )\n\t\t{\n\t\t\tfloat a = 0;\n\t\t\tfloat avg = 0;\n\t\t\tfloat w = 1;\n\t\t\tfloat frq = 1;\n\n\t\t\tfor ( int i = 0; i < n; i++ )\n\t\t\t{\n\t\t\t\ta += Compute( x * frq, y * frq ) * w;\n\t\t\t\tavg += w;\n\t\t\t\tw *= 0.25f;\n\t\t\t\tfrq *= 3;\n\t\t\t}\n\n\t\t\treturn a / avg;\n\t\t}\n\n\t\t//\"Normal\" Value-Noise\n\t\tstatic public float ComputeLinear( float x, float y )\n\t\t{\n\t\t\tint ix = (int)x;\n\t\t\tint iy = (int)y;\n\n\t\t\tfloat dx = x - ix;\n\t\t\tfloat dy = y - iy;\n\n\t\t\tfloat fx1 = lerp( Random( ix, iy ), Random( ix + 1, iy ), dx );\n\t\t\tfloat fx2 = lerp( Random( ix, iy + 1 ), Random( ix + 1, iy + 1 ), dx );\n\n\t\t\treturn lerp( fx1, fx2, dy );\n\t\t}\n\n\n\t\t//Basic interpolation Functions\n\n\t\t//7th order smoothstep\n\t\tstatic float smootherstep( float a1, float a2, float t )\n\t\t{\n\t\t\treturn lerp( a1, a2, t * t * t * t * (t * (t * (70 - 20 * t) - 84) + 35) );\n\t\t}\n\n\t\t//Normal smoothstep\n\t\tstatic float smoothstep( float a1, float a2, float t )\n\t\t{\n\t\t\treturn lerp( a1, a2, t * t * (3 - 2 * t) );\n\t\t}\n\n\t\tstatic float lerp( float a1, float a2, float t )\n\t\t{\n\t\t\tt = Math.Clamp( t, 0f, 1f );\n\n\t\t\treturn (1 - t) * a1 + t * a2;\n\t\t}\n\t}\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainShapes/Volcanic.cs",
"FileName": "Volcanic.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using Editor;\nusing Sandbox;\nusing Sandbox.UI;\nusing System;\n\nnamespace Sturnus.TerrainGenerationTool;\npublic static class Volcanic\n{\n\tpublic static float Default(\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tlong seed,\n\t\tfloat minHeight,\n\t\tbool warp,\n\t\tfloat warpSize,\n\t\tfloat warpStrength\n\t)\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\n\t\tfloat craterRadius = 0.3f; // Average radius of the central crater\n\t\tfloat rimHeight = 0.25f; // Height of the crater rim\n\t\tfloat rimWidth = 0f; // Width of the rim\n\t\tfloat outerSlopeStrength = 0.5f; // Strength of the gradient for the exterior slope\n\t\tfloat innerSlopeStrength = 2.0f; // Strength of the gradient for the interior slope\n\t\tfloat baseHeight = 0.1f; // Minimum base height\n\t\tfloat noiseStrength = 0.02f; // General noise strength\n\t\tfloat distance = MathF.Sqrt( nx * nx + ny * ny );\n\n\t\t// Calculate distance from the center of the heightmap\n\t\tfloat centerX = 0f, centerY = 0f; // Center of the heightmap\n\t\tfloat distanceToCenter = MathF.Sqrt( (nx - centerX) * (nx - centerX) + (ny - centerY) * (ny - centerY) );\n\n\t\t// Apply domain warping for irregularity\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Introduce irregularity to the crater radius\n\t\tfloat craterIrregularity = OpenSimplex2S.Noise2( seed + 20, nx * 4.0f, ny * 4.0f ) * 0.1f;\n\t\tfloat dynamicCraterRadius = craterRadius + craterIrregularity;\n\n\t\t// Initialize height components\n\t\tfloat crater = 0f;\n\t\tfloat rim = 0f;\n\t\tfloat outerSlope = 0f;\n\n\t\t// Crater: Smooth inward slope towards the center of the crater\n\t\tif ( distanceToCenter < dynamicCraterRadius )\n\t\t{\n\t\t\tfloat craterDepth = (1f - (distanceToCenter));\n\t\t\tcrater = MathF.Pow( craterDepth, innerSlopeStrength ) ; // Inward slope\n\t\t}\n\n\t\t// Rim: Uneven ridge around the crater\n\t\tif ( distanceToCenter >= dynamicCraterRadius && distanceToCenter < dynamicCraterRadius + rimWidth )\n\t\t{\n\t\t\tfloat rimFalloff = (distanceToCenter - dynamicCraterRadius) / rimWidth;\n\t\t\tfloat rimNoise = OpenSimplex2S.Noise2( seed + 30, nx * 8.0f, ny * 8.0f ) * noiseStrength;\n\t\t\trim = (1f - rimFalloff) * rimHeight + rimNoise; // Add noise for unevenness\n\t\t}\n\n\t\t// Outer slope: Smooth gradient with noise towards the edges\n\t\tif ( distanceToCenter >= dynamicCraterRadius + rimWidth )\n\t\t{\n\t\t\tfloat slopeDistance = 1f - distanceToCenter; // Decrease height as we approach the edge\n\t\t\tfloat slopeNoise = OpenSimplex2S.Noise2( seed + 40, nx * 4.0f, ny * 4.0f ) * noiseStrength;\n\t\t\touterSlope = MathF.Max( 0, slopeDistance ) * outerSlopeStrength + slopeNoise;\n\t\t}\n\n\t\t// Combine components\n\t\tfloat heightValue = baseHeight + crater + rim + outerSlope;\n\n\t\t// Smooth transition towards the crater center for a more natural look\n\t\tif ( distanceToCenter < dynamicCraterRadius )\n\t\t{\n\t\t\tfloat centerFalloff = MathF.Pow( 1f - (distanceToCenter / dynamicCraterRadius), 2f );\n\t\t\theightValue = centerFalloff * baseHeight; // Slight bump for a smoother slope\n\t\t}\n\n\n\t\theightValue = Math.Max( heightValue, baseHeight );\n\n\t\tvar heightValueBase = Math.Max( heightValue, minHeight );\n\n\t\t// Clamp the final height\n\t\treturn Math.Clamp( heightValueBase, 0, 1 );\n\t}\n\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainShapes/Mountainous.cs",
"FileName": "Mountainous.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using Editor;\nusing Sandbox;\nusing System;\n\nnamespace Sturnus.TerrainGenerationTool;\npublic static class Mountainous\n{\n\tpublic static float Default( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize = 0.1f, float warpStrength = 0.5f )\n\t{\n\t\tfloat nx = x / (float)width; // Normalize x to range [0, 1]\n\t\tfloat ny = y / (float)height; // Normalize y to range [0, 1]\n\t\tfloat warpX;\n\t\tfloat warpY;\n\t\tfloat warpedNx;\n\t\tfloat warpedNy;\n\n\t\tif( warp )\n\t\t{\n\t\t\t// Generate warp offsets using noise\n\t\t\twarpX = OpenSimplex2S.Noise2( seed + 20, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\twarpY = OpenSimplex2S.Noise2( seed + 21, nx * warpSize, ny * warpSize ) * warpStrength;\n\n\t\t\t// Apply domain warping to the coordinates\n\t\t\twarpedNx = nx + warpX;\n\t\t\twarpedNy = ny + warpY;\n\t\t}\n\t\telse\n\t\t{\n\t\t\twarpedNx = nx;\n\t\t\twarpedNy = ny;\n\t\t}\n\t\t\n\n\t\t// Base noise for ridge structure (using warped coordinates)\n\t\tfloat ridgeNoise = Math.Abs( OpenSimplex2S.Noise2( seed, warpedNx * 5, warpedNy * 0.5f ) ) * 0.8f;\n\n\t\t// Add distortion to the ridge line to make it less uniform\n\t\tfloat distortion = OpenSimplex2S.Noise2( seed + 2, warpedNx * 2, warpedNy * 2 ) * 0.3f;\n\t\tridgeNoise += distortion;\n\n\t\t// Add fine detail to the mountains with higher frequency noise\n\t\tfloat detailNoise = OpenSimplex2S.Noise2( seed + 1, warpedNx * 20, warpedNy * 20 ) * 0.2f;\n\n\t\t// Combine ridge, distortion, and detail noise\n\t\tfloat combinedNoise = ridgeNoise + detailNoise;\n\n\t\t// Apply a falloff effect to keep the edges lower\n\t\tfloat edgeFalloff = 1.0f - Math.Clamp( Math.Abs( nx - 0.5f ) + Math.Abs( ny - 0.5f ), 0, 1 );\n\n\t\t// Combine all effects\n\t\tfloat heightValue = combinedNoise * edgeFalloff;\n\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\tfloat baseline = minHeight; // Minimum height\n\t\theightValue = MathF.Max( heightValue, baseline );\n\n\t\t// Combine everything with edge falloff\n\t\treturn Math.Clamp( heightValue, 0, 1 );\n\t}\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainShapes/Sea.cs",
"FileName": "Sea.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using Editor;\nusing Sandbox;\nusing System;\n\nnamespace Sturnus.TerrainGenerationTool;\npublic static class Sea\n{\n\tpublic static float SeaBed(\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tlong seed,\n\t\tfloat minHeight,\n\t\tbool domainWarping,\n\t\tfloat domainWarpingSize,\n\t\tfloat domainWarpingStrength\n\t)\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat depthScale = 0.4f; // Adjust the overall depth\n\t\tfloat waveFrequency = 0.5f; // Frequency of base ripples\n\t\tfloat waveAmplitude = 0.1f; // Height of ripples\n\t\tfloat randomVariation = 0.02f; // Subtle randomness\n\t\tfloat distortionFrequency = 0.03f; // Frequency for distortion\n\t\tfloat distortionStrength = 0.05f ;\t// Strength of distortion\n\t\t// Normalize coordinates to [0, 1]\n\t\tfloat nx = x / (float)width;\n\t\tfloat ny = y / (float)height;\n\n\t\t// Generate base ripple effect\n\t\tfloat baseRipple = MathF.Sin( nx * waveFrequency * MathF.PI * 2 ) * waveAmplitude\n\t\t\t\t\t\t + MathF.Sin( ny * waveFrequency * MathF.PI * 2 ) * waveAmplitude;\n\n\t\t// Add distortion to break uniformity\n\t\tfloat distortion = OpenSimplex2S.Noise2( seed + 1, nx * distortionFrequency, ny * distortionFrequency )\n\t\t\t\t\t\t * distortionStrength;\n\n\t\t// Add random noise for natural variation\n\t\tfloat randomNoise = (float)(random.NextDouble() - 0.5) * randomVariation;\n\n\t\t// Combine all effects\n\t\tfloat heightValue = baseRipple + distortion + randomNoise;\n\t\t\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\tfloat baseline = minHeight; // Minimum height\n\t\theightValue = MathF.Max( heightValue, baseline );\n\n\t\t// Scale to depth and clamp\n\t\theightValue = heightValue * depthScale;\n\t\treturn Math.Clamp( heightValue, 0.0f, 1.0f );\n\t}\n\n\tpublic static float Cliff(\n\tint x,\n\tint y,\n\tint width,\n\tint height,\n\tlong seed,\n\tfloat minHeight,\n\tbool warp,\n\tfloat warpSize,\n\tfloat warpStrength\n)\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\t\tfloat hillHeight = 0.9f; // Height of the cliff\n\t\tfloat slopeWidth = 0.2f; // Width of the slope transition\n\t\tfloat wideningFactor = 0.9f;\n\t\t// Apply domain warping for cliff irregularity\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Calculate distance for the hill gradient\n\t\tfloat distance = nx >= 0 ? MathF.Abs( nx ) : MathF.Abs( nx ) * (1 - wideningFactor); // Widen on one side\n\n\t\t// Generate hill gradient using a smooth transition\n\t\tfloat hill = Math.Clamp( 1.0f - MathF.Pow( distance / slopeWidth, 2.0f ), 0, 1 ); // Quadratic falloff for smoother slope\n\t\thill *= hillHeight; // Scale the hill to the desired height\n\n\t\t// Add base noise for texture\n\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed, nx * 6.0f, ny * 6.0f ) * 0.2f;\n\n\t\t// Add finer noise for additional detail\n\t\tfloat fineNoise = OpenSimplex2S.Noise2( seed + 1, nx * 12.0f, ny * 12.0f ) * (0.2f / 2);\n\n\t\t// Combine hill gradient with noise\n\t\tfloat heightValue = hill + baseNoise + fineNoise;\n\n\t\tfloat baseValue = Math.Max( baseNoise, minHeight );\n\t\theightValue = Math.Max( heightValue, baseValue );\n\n\t\t// Clamp the height value to ensure valid results\n\t\treturn Math.Clamp( heightValue, 0, 1 );\n\t}\n\n\n\n\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainGenerationTool.cs",
"FileName": "TerrainGenerationTool.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel.DataAnnotations.Schema;\r\nusing System.Drawing;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing Editor;\r\nusing Editor.ShaderGraph.Nodes;\r\nusing Editor.Widgets;\r\nusing Sandbox;\r\nusing SkiaSharp;\r\nusing static Sandbox.Gradient;\r\n\r\nusing Sturnus.TerrainGenerationTool;\r\nusing Sandbox.Utility;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Parallel = System.Threading.Tasks.Parallel;\r\nusing Sturnus.TerrainGenerationTool.RiverStream;\r\nusing Sandbox.Services;\r\nusing System.Reflection;\r\nusing static TerrainGenerationTool;\r\n\r\n[EditorApp( \"Terrain Pro Generation\", \"terrain\", \"Generate procedural terrain with a realtime 3D preview\" )]\r\npublic class TerrainGenerationTool : BaseWindow\r\n{\r\n\tpublic string GenerationPath { get; set; } = Editor.FileSystem.Content.GetFullPath( \"\" ) + \"\\\\TerrainGenerationTool\\\\\";\r\n\tpublic string GenerationLocalPath { get; set; } = \"\\\\TerrainGenerationTool\\\\\";\r\n\tpublic string ExportPath { get; set; } = Project.Current.RootDirectory + \"\\\\Assets\\\\\";\r\n\r\n\tHashSet<string> TerrainCategoryArray { get; set; } = new HashSet<string>();\r\n\tHashSet<string> TerrainShapeArray { get; set; } = new HashSet<string>();\r\n\r\n\t// Per-tile category/shape selections for the tile grid (index = ty * grid + tx)\r\n\tstring[] _tileCategories = new string[1];\r\n\tstring[] _tileShapes = new string[1];\r\n\r\n\t// Per-tile height/scale/seed values (index = ty * grid + tx)\r\n\tfloat[] _tileMinHeights = new float[1];\r\n\tfloat[] _tileMaxHeights = new float[1];\r\n\tfloat[] _tilePlaneScales = new float[1];\r\n\tlong[] _tileSeeds = new long[1];\r\n\r\n\t// Per-tile smoothing/noise values (index = ty * grid + tx)\r\n\tint[] _tileSmoothingPasses = new int[1];\r\n\tint[] _tileNoiseLayerStacks = new int[1];\r\n\r\n\t// Per-tile domain warping values (index = ty * grid + tx)\r\n\tbool[] _tileDomainWarping = new bool[1];\r\n\tfloat[] _tileDomainWarpingSizes = new float[1];\r\n\tfloat[] _tileDomainWarpingStrengths = new float[1];\r\n\r\n\t// Per-tile splatmap settings (index = ty * grid + tx)\r\n\tint[] _tileSplatLayerCounts = new int[1];\r\n\tint[] _tileSplatMapCounts = new int[1];\r\n\tSplatDispersionMode[] _tileSplatDispersions = new SplatDispersionMode[1];\r\n\tfloat[] _tileSplatBlendStrengths = new float[1];\r\n\r\n\t// The tile currently being edited by the Terrain Type page's Category/Shape selectors\r\n\tint _selectedTileIndex = 0;\r\n\tbool _syncingTileSelectors;\r\n\tList<TileGridBox> _tileBoxes = new();\r\n\r\n\tList<Type> terrainCategoryClassesTypes = new List<Type> { typeof( Islands ), typeof( Mountainous ), typeof( Planetary ), typeof( Realistic ), typeof( Sea ), typeof( Volcanic ) };\r\n\tList<Type> terrainShapeMethodTypes { get; set; }\r\n\r\n\r\n\tenum TerrainDimensions : int\r\n\t{\r\n\t\tx512 = 512,\r\n\t\tx1024 = 1024,\r\n\t\tx2048 = 2048,\r\n\t\tx4096 = 4096,\r\n\t\tX8192 = 8192\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// How the grid is stored once generated.\r\n\t/// Combined stitches every cell into one full-res map; PerCell keeps each cell as its own\r\n\t/// full-resolution heightmap/splatmap (for per-terrain apply, per-cell export and preview).\r\n\t/// </summary>\r\n\tpublic enum GridStorageMode\r\n\t{\r\n\t\tCombined,\r\n\t\tPerCell\r\n\t}\r\n\r\n\t[Step( 1 )] GridStorageMode GridStorage { get; set; } = GridStorageMode.Combined;\r\n\r\n\t// Per-cell full-resolution maps (index = ty * grid + tx). Only filled in PerCell mode.\r\n\tList<float[,]> _cellHeightmaps = new();\r\n\tList<float[,]> _cellSplatmaps = new();\r\n\r\n\tpublic enum SplatDispersionMode\r\n\t{\r\n\t\tEvenly,\r\n\t\tNatural\r\n\t}\r\n\r\n\t//enum TerrainCategoryEnum;\r\n\tDynamicEnum TerrainCategoryEnum = new DynamicEnum();\r\n\tDynamicEnum TerrainShapeEnum = new DynamicEnum();\r\n\r\n\tTerrainDimensions TerrainDimensionsEnum { get; set; } = TerrainDimensions.x512;\r\n\t[Step( 1 ), MinMax( 1, 4 )] int TerrainGridSize { get; set; } = 1;\r\n\t//TerrainCategoryEnum TerrainShapeEnumSelect { get; set; }\r\n\t[Step( 0.01f ),MinMax(0.1f,1f)] float TerrainMinHeight { get; set; } = 0.2f;\r\n\t[Step( 0.01f),MinMax(0.1f,1f)] float TerrainMaxHeight { get; set; } = 0.5f;\r\n\t[Step( 0.01f),MinMax(0.1f,1f)] float TerrainPlaneScale { get; set; } = 0.5f;\r\n\tlong TerrainSeed { get; set; } = 1234567890;\r\n\t[Step( 1 ), MinMax( 1,20 )] int SmoothingPasses { get; set; } = 10;\r\n\t[Group( \"Domain Warping\" )] bool DomainWarping { get; set; } = true;\r\n\t[Group( \"Domain Warping\" )][Step( 0.01f ),MinMax(0.1f,1f)] float DomainWarpingSize { get; set; } = 0.25f;\r\n\t[Group( \"Domain Warping\" )][Step( 0.01f ),MinMax(0.1f,1f)] float DomainWarpingStrength { get; set; } = 0.15f;\r\n\tbool ErosionSimulation { get; set; } = false;\r\n\t[Step( 1f),MinMax(1f,25f)] int NoiseLayerStacks { get; set; } = 1;\r\n\r\n\t///\r\n\t/// River Carving Variables\r\n\t///\r\n\t[Property][Group( \"River & Stream Carving\" )] bool RiverCarvingBool { get; set; } = true;\r\n\t[Property][Group( \"River & Stream Carving\" )][Step( 0.1f), MinMax( 0.5f, 10f )] float RiverCarvingFrequency { get; set; } = 1.5f;\r\n\t[Property][Group( \"River & Stream Carving\" )][Step( 0.01f),MinMax(0.01f,5f)] float RiverCarvingStrength { get; set; } = 0.3f;\r\n\t[Property][Group( \"River & Stream Carving\" )][Step( 0.001f),MinMax(0.01f,0.25f)] float RiverCarvingDepth { get; set; } = 0.01f;\r\n\t[Property][Group( \"River & Stream Carving\" )][Step( 0.01f),MinMax(0.001f,2f)] float RiverCarvingWidth { get; set; } = 0.25f;\r\n\t[Property][Group( \"River & Stream Carving\" )][Step( 0.01f),MinMax(0.05f,1f)] float RiverCarvingSpacing { get; set; } = 0.05f;\r\n\t[Property][Group( \"River & Stream Carving\" )][Step( 0.01f),MinMax(0.01f,1f)] float RiverCarvingTurbulenceStrength { get; set; } = 0.01f;\r\n\t[Property][Group( \"River & Stream Carving\" )][Step( 0.01f),MinMax(0.01f,10f)] float RiverCarvingTurbulenceFrequency { get; set; } = 0.01f;\r\n\t\r\n\r\n\t///\r\n\t/// Tool Placement Square\r\n\t///\r\n\t[Group( \"Tool Placement\" )] bool StagingArea { get; set; } = true;\r\n\t[Group( \"Tool Placement\" )][Step( 1),MinMax( 1, 100 )] int StagingAreaSize { get; set; } = 10; // Size of the square (in grid units)\r\n\t[Group( \"Tool Placement\" )][Step(0.01f),MinMax(0,1)] float StagingAreaHeight { get; set; } = 0.1f; // Height of the flat square\r\n\t[Group( \"Tool Placement\" )][Step(0.01f),MinMax(0,1)] float StagingAreaX { get; set; } = 0.1f; // X-center of the square as a ratio\r\n\t[Group( \"Tool Placement\" )][Step(0.01f),MinMax(0,1)] float StagingAreaY { get; set; } = 0.1f; // Y-center of the square as a ratio\r\n\r\n\tGradient SplatMapGradient = new Gradient( new Gradient.ColorFrame( 0.0f, Color.Cyan ), new Gradient.ColorFrame( 0.25f, Color.Red ), new Gradient.ColorFrame( 0.5f, Color.Yellow ), new Gradient.ColorFrame( 0.75f, Color.Green ) );\r\n\tSKColor[] _splatcolors { get; set; }\r\n\r\n\t[Step( 1 ), MinMax( 2, 32 )] int SplatLayerCount { get; set; } = 8;\r\n\t[Step( 1 ), MinMax( 1, 8 )] int SplatMapCount { get; set; } = 1;\r\n\tSplatDispersionMode SplatDispersion { get; set; } = SplatDispersionMode.Evenly;\r\n\t[Step( 0.05f ), MinMax( 0f, 1f )] float SplatBlendStrength { get; set; } = 0.35f;\r\n\r\n\t[Property] bool PreviewSplatMaterials { get; set; } = false;\r\n\r\n\tfloat[] _splatthresholds = { 0f, 0.25f, 0.50f, 0.75f };\r\n\tfloat[,] _heightmap;\r\n\tfloat[,] _splatmap;\r\n\tfloat[,] _previewHeightmap;\r\n\r\n\tTerrainMaterial[] _previewMaterials;\r\n\tint _previewMaterialsGeneration = 0;\r\n\tList<Editor.Asset> _localTmatAssets;\r\n\r\n\tTexture _preview_image_texture;\r\n\tEditor.TextureWidget PreviewImage;\r\n\tTexture _preview_splatmap_texture;\r\n\tEditor.TextureWidget PreviewSplatmap;\r\n\r\n\tSceneRenderingWidget RenderCanvas;\r\n\tCameraComponent Camera;\r\n\tGizmo.Instance GizmoInstance;\r\n\tGameObject _previewGO;\r\n\tTerrain _previewTerrain;\r\n\tTerrainStorage _previewStorage;\r\n\tGameObject _splatOverlayGO;\r\n\tModelRenderer _splatOverlayRenderer;\r\n\tMesh _overlayMesh;\r\n\tfloat[] _overlayTargetHeights;\r\n\tfloat[] _overlayCurrentHeights;\r\n\tColor32[] _overlayCurrentColors;\r\n\tfloat[,] _overlaySplatmap;\r\n\tbool _overlayUseSplatColors;\r\n\tbool _overlayAnimating;\r\n\tbool _overlayColorAnimating;\r\n\tList<Color> _splatColorCache = new();\r\n\tList<float> _currentFrameTimes;\r\n\tList<float> _targetFrameTimes;\r\n\tbool _gradientAnimating;\r\n\tbool _isAnimatingGradient;\r\n\tconst float PreviewMorphSpeed = 8f;\r\n\tconst float MeshMorphSpeed = PreviewMorphSpeed * 0.25f;\r\n\tfloat _orbitDistance = 20000f;\r\n\tfloat _orbitAngle = 0f;\r\n\tfloat _orbitPitch = 30f;\r\n\tbool _autoSpin = true;\r\n\tFloatSlider ZoomSlider;\r\n\tconst float SpinSpeed = 8f; // degrees per second\r\n\tconst int PreviewResolution = 512;\r\n\tconst float PreviewTerrainSize = 20000f;\r\n\tconst float PreviewTerrainHeight = 5000f;\r\n\r\n\tSerializedObject _serialized;\r\n\tbool _previewDirty;\r\n\tfloat _lastPreviewRegen = float.MinValue;\r\n\tbool _isGenerating = false;\r\n\tint _generationToken = 0;\r\n\r\n\tList<Widget> _domainWarpingWidgets = new();\r\n\tList<Widget> _riverCarvingWidgets = new();\r\n\tList<Widget> _stagingAreaWidgets = new();\r\n\r\n\tDictionary<string, Widget> _propsPages = new();\r\n\tSegmentedControl _propsTabBar;\r\n\tWidget _propsContent;\r\n\tstring _activePropsTab;\r\n\tWidget _tilesContainer;\r\n\tButton _randomizeMaterialsButton;\r\n\tLabel _materialLoadingLabel;\r\n\tGradientControlWidget _gradientControlWidget;\r\n\r\n\tWrapSelector ShapeArray;\r\n\tWrapSelector CategoryArray;\r\n\r\n\tpublic class DynamicEnum\r\n\t{\r\n\t\tprivate readonly Dictionary<string, int> _values = new Dictionary<string, int>();\r\n\t\tprivate int _nextValue = 0;\r\n\r\n\t\tpublic void Add( string name )\r\n\t\t{\r\n\t\t\tif ( !_values.ContainsKey( name ) )\r\n\t\t\t{\r\n\t\t\t\t_values[name] = _nextValue++;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tpublic int GetValue( string name )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrEmpty( name ) ) return -1;\r\n\t\t\treturn _values.TryGetValue( name, out var value ) ? value : -1; // Return -1 if not found\r\n\t\t}\r\n\r\n\t\tpublic string GetName( int key )\r\n\t\t{\r\n\t\t\treturn _values.FirstOrDefault( pair => pair.Value == key ).Key ?? \"Unknown\"; // Return \"Unknown\" if not found\r\n\t\t}\r\n\r\n\t\tpublic string[] GetNames()\r\n\t\t{\r\n\t\t\treturn _values.Keys.ToArray();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static string[] GetMethodsFromClass( string className )\r\n\t{\r\n\t\t// Attempt to get the Type from the class name (fully qualified)\r\n\t\tType classType = Type.GetType( className );\r\n\r\n\t\tif ( classType == null )\r\n\t\t{\r\n\t\t\tthrow new ArgumentException( $\"Class '{className}' could not be found. Ensure the namespace is included.\" );\r\n\t\t}\r\n\r\n\t\tList<string> methodNames = new List<string>();\r\n\r\n\t\t// Get all public methods (static and instance) from the class\r\n\t\tMethodInfo[] methods = classType.GetMethods( BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static );\r\n\r\n\t\tforeach ( var method in methods )\r\n\t\t{\r\n\t\t\t// Exclude methods not declared in this class\r\n\t\t\tif ( method.DeclaringType == classType )\r\n\t\t\t{\r\n\t\t\t\tmethodNames.Add( method.Name );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn methodNames.ToArray();\r\n\t}\r\n\r\n\tpublic string[] GetTerrainCategoryClasses( params Type[] CategoryClasses )\r\n\t{\r\n\t\tHashSet<string> methodNames = new HashSet<string>();\r\n\r\n\t\tforeach ( Type CategoryClass in CategoryClasses )\r\n\t\t{\r\n\t\t\t// Get all public static methods from the class\r\n\t\t\tMethodInfo[] methods = CategoryClass.GetMethods( BindingFlags.Public | BindingFlags.Static );\r\n\r\n\t\t\tforeach ( MethodInfo method in methods )\r\n\t\t\t{\r\n\t\t\t\t// Exclude inherited methods or non-relevant ones\r\n\t\t\t\tif ( method.DeclaringType == CategoryClass )\r\n\t\t\t\t{\r\n\t\t\t\t\tmethodNames.Add( $\"{CategoryClass.Name}.{method.Name}\" );\r\n\t\t\t\t\tTerrainCategoryEnum.Add( CategoryClass.Name);\r\n\t\t\t\t\tTerrainCategoryArray.Add( CategoryClass.Name );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn methodNames.ToArray();\r\n\t}\r\n\r\n\tpublic string[] GetTerrainShapeMethods( Type shapeClass )\r\n\t{\r\n\t\tHashSet<string> methodNames = new HashSet<string>();\r\n\t\tMethodInfo[] methods = shapeClass.GetMethods( BindingFlags.Public | BindingFlags.Static );\r\n\r\n\t\t\tforeach ( MethodInfo method in methods )\r\n\t\t\t{\r\n\t\t\t\t// Exclude inherited methods or non-relevant ones\r\n\t\t\t\tif ( method.DeclaringType == shapeClass )\r\n\t\t\t\t{\r\n\t\t\t\t\t//TerrainShapeArray.Add( shapeClass.Name );\r\n\t\t\t\t\t//TerrainShapeEnum.Add( shapeClass.Name );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\treturn methodNames.ToArray();\r\n\t}\r\n\r\n\tpublic static object CallMethod( string className, string methodName, object[] parameters = null )\r\n\t{\r\n\t\t// Get the class type\r\n\t\tType classType = Type.GetType( className );\r\n\t\tif ( classType == null )\r\n\t\t{\r\n\t\t\tthrow new ArgumentException( $\"Class '{className}' could not be found.\" );\r\n\t\t}\r\n\r\n\t\t// Get the method info\r\n\t\tMethodInfo method = classType.GetMethod( methodName, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance );\r\n\t\tif ( method == null )\r\n\t\t{\r\n\t\t\tthrow new ArgumentException( $\"Method '{methodName}' could not be found in class '{className}'.\" );\r\n\t\t}\r\n\r\n\t\t// Check if the method is static or instance\r\n\t\tobject instance = null;\r\n\t\tif ( !method.IsStatic )\r\n\t\t{\r\n\t\t\tinstance = Activator.CreateInstance( classType );\r\n\t\t}\r\n\r\n\t\t// Invoke the method\r\n\t\tobject result = method.Invoke( instance, parameters );\r\n\r\n\t\t// Ensure the return type is compatible\r\n\t\tif ( result is not float )\r\n\t\t{\r\n\t\t\tthrow new InvalidOperationException( $\"Method '{methodName}' does not return a float.\" );\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\tpublic void InitialShapes()\r\n\t{\r\n\t\tShapeArray.DestroyChildren();\r\n\t\tTerrainShapeArray.Clear();\r\n\r\n\t\tstring className = $\"Sturnus.TerrainGenerationTool.Islands\"; // Fully qualified name\r\n\t\tstring[] methods = GetMethodsFromClass( className );\r\n\r\n\t\t// Print the methods\r\n\t\tforeach ( string method in methods )\r\n\t\t{\r\n\t\t\tTerrainShapeArray.Add( method );\r\n\t\t\tTerrainShapeEnum.Add( method );\r\n\t\t}\r\n\r\n\t\tforeach ( var shape in TerrainShapeArray )\r\n\t\t{\r\n\t\t\tShapeArray.AddOption( shape );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic TerrainGenerationTool() : base()\r\n\t{\r\n\t\tWindowTitle = \"Terrain Generation Tool\";\r\n\t\tSetWindowIcon( \"terrain\" );\r\n\t\tMinimumSize = new Vector2( 1000, 700 );\r\n\t\tSize = new Vector2( 1500, 900 );\r\n\t\tStartCentered = true;\r\n\r\n\t\t_serialized = this.GetSerialized();\r\n\t\t_serialized.OnPropertyChanged += OnSerializedPropertyChanged;\r\n\r\n\t\tstring[] terrainCategoryClasses = GetTerrainCategoryClasses( terrainCategoryClassesTypes.ToArray() );\r\n\t\tstring[] terrainShapeMethods = GetTerrainShapeMethods( typeof(Islands) );\r\n\t\t\r\n\t\t//Create TerrainGenerationTool folder if it doesn't exist.\r\n\t\tDirectory.CreateDirectory( GenerationPath );\r\n\r\n\t\tSplatMapGradient.Blending = Gradient.BlendMode.Stepped;\r\n\r\n\t\tLayout = Layout.Row();\r\n\t\tLayout.Margin = 0;\r\n\t\tLayout.Spacing = 0;\r\n\r\n\t\t// ---------- Left: options panel ----------\r\n\t\tvar scroll = new ScrollArea( this );\r\n\t\tscroll.Canvas = new Widget( scroll );\r\n\t\tscroll.Canvas.Layout = Layout.Column();\r\n\t\tscroll.Canvas.Layout.Margin = 10;\r\n\t\tscroll.Canvas.Layout.Spacing = 5;\r\n\t\tscroll.MinimumWidth = 400;\r\n\t\tscroll.MaximumWidth = 480;\r\n\t\tLayout.Add( scroll );\r\n\r\n\t\tvar body = scroll.Canvas.Layout;\r\n\r\n\t\t// ---------- Left: grouped property tabs ----------\r\n\t\tvar propsRoot = body.Add( new Widget( null ), 1 );\r\n\t\tpropsRoot.Layout = Layout.Column();\r\n\t\tpropsRoot.Layout.Spacing = 5;\r\n\r\n\t\t_propsTabBar = propsRoot.Layout.Add( new SegmentedControl() );\r\n\t\t_propsTabBar.ShowText = true;\r\n\t\t_propsTabBar.FixedHeight = Theme.RowHeight * 1.6f;\r\n\t\t_propsTabBar.OnSelectedChanged += ( name ) => SelectPropsTab( name );\r\n\r\n\t\t_propsContent = propsRoot.Layout.Add( new Widget( null ), 1 );\r\n\t\t_propsContent.Layout = Layout.Column();\r\n\t\t_propsContent.Layout.Margin = 0;\r\n\t\t_propsContent.Layout.Alignment = TextFlag.Top;\r\n\r\n\t\t// --- Terrain Type tab ---\r\n\t\tvar typePage = CreatePropsPage();\r\n\r\n\t\ttypePage.Layout.Add( new Label( \"Terrain Dimensions\" ) );\r\n\t\ttypePage.Layout.Add( new EnumControlWidget( _serialized.GetProperty( nameof( TerrainDimensionsEnum ) ) ) );\r\n\r\n\t\ttypePage.Layout.Add( new Label( \"Terrain Category\" ) );\r\n\t\tCategoryArray = typePage.Layout.Add( new WrapSelector() );\r\n\t\tfor ( int i = 0; i < TerrainCategoryArray.ToArray().GetLength( 0 ); i++ )\r\n\t\t{\r\n\t\t\tList<string> rowValues = new List<string>();\r\n\t\t\trowValues.Add( TerrainCategoryArray.ToArray()[i] );\r\n\t\t\tCategoryArray.AddOption( rowValues[0] );\r\n\t\t}\r\n\t\ttypePage.Layout.Add( new Label( \"Terrain Shape\" ) );\r\n\t\tShapeArray = typePage.Layout.Add( new WrapSelector() );\r\n\t\tInitialShapes();\r\n\t\tCategoryArray.OnSelectedChanged += ( _ ) =>\r\n\t\t{\r\n\t\t\tRebuildShapes();\r\n\t\t\tApplySelectedCategory();\r\n\t\t\t_previewDirty = true;\r\n\t\t};\r\n\t\tShapeArray.OnSelectedChanged += ( _ ) =>\r\n\t\t{\r\n\t\t\tApplySelectedShape();\r\n\t\t\t_previewDirty = true;\r\n\t\t};\r\n\t\tif ( CategoryArray.Children.Count() > 0 )\r\n\t\t{\r\n\t\t\tCategoryArray.SelectedIndex = 0;\r\n\t\t\tCategoryArray.Selected = CategoryArray.Children.First().Name;\r\n\t\t}\r\n\t\tRebuildShapes();\r\n\t\tif ( ShapeArray.Children.Count() > 0 )\r\n\t\t{\r\n\t\t\tShapeArray.SelectedIndex = 0;\r\n\t\t\tShapeArray.Selected = ShapeArray.Children.First().Name;\r\n\t\t}\r\n\t\tAddPropsTab( \"Terrain Type\", \"terrain\", typePage, \"Terrain dimensions, category and shape\" );\r\n\r\n\t\t// --- Height / Scale tab ---\r\n\t\tvar heightPage = CreatePropsPage();\r\n\r\n\t\theightPage.Layout.Add( new Label( \"Min Height (relative)\" ) );\r\n\t\theightPage.Layout.Add( FloatSlider( nameof( TerrainMinHeight ) ) );\r\n\t\theightPage.Layout.Add( new Label( \"Max Height (relative)\" ) );\r\n\t\theightPage.Layout.Add( FloatSlider( nameof( TerrainMaxHeight ) ) );\r\n\t\theightPage.Layout.Add( new Label( \"Terrain Plane Scale\" ) );\r\n\t\theightPage.Layout.Add( FloatSlider( nameof( TerrainPlaneScale ) ) );\r\n\t\theightPage.Layout.Add( new Label( \"Terrain Seed\" ) );\r\n\t\tvar seedRow = heightPage.Layout.AddRow();\r\n\t\tseedRow.Spacing = 4;\r\n\t\tvar seedControl = seedRow.Add( new IntegerControlWidget( _serialized.GetProperty( nameof( TerrainSeed ) ) ), 1 );\r\n\t\tseedRow.Add( new IconButton( \"casino\", RandomizeSeed, this )\r\n\t\t{\r\n\t\t\tToolTip = \"Randomize seed\",\r\n\t\t\tIconSize = 16,\r\n\t\t\tFixedSize = new Vector2( 26, 26 )\r\n\t\t} );\r\n\t\tAddPropsTab( \"Height/Scale\", \"straighten\", heightPage, \"Terrain height, plane scale and seed\" );\r\n\r\n\t\t// --- Smooth / Noise tab ---\r\n\t\tvar noisePage = CreatePropsPage();\r\n\r\n\t\tnoisePage.Layout.Add( new Label( \"Smoothing Passes\" ) );\r\n\t\tnoisePage.Layout.Add( IntSlider( nameof( SmoothingPasses ) ) );\r\n\t\tnoisePage.Layout.Add( new Label( \"Noise Layer Stacks\" ) );\r\n\t\tnoisePage.Layout.Add( IntSlider( nameof( NoiseLayerStacks ) ) );\r\n\t\tAddPropsTab( \"Smooth/Noise\", \"grain\", noisePage, \"Terrain smoothing and noise layers\" );\r\n\r\n\t\t// --- Splat tab ---\r\n\t\tvar splatPage = CreatePropsPage();\r\n\r\n\t\tsplatPage.Layout.Add( new Label( \"Splat Layer Count\" ) );\r\n\t\tsplatPage.Layout.Add( IntSlider( nameof( SplatLayerCount ) ) );\r\n\t\tsplatPage.Layout.Add( new Label( \"Splat Map Count\" ) );\r\n\t\tsplatPage.Layout.Add( IntSlider( nameof( SplatMapCount ) ) );\r\n\t\tsplatPage.Layout.Add( new Label( \"Dispersion\" ) );\r\n\t\tsplatPage.Layout.Add( new EnumControlWidget( _serialized.GetProperty( nameof( SplatDispersion ) ) ) );\r\n\t\tsplatPage.Layout.Add( new Label( \"Blend Strength\" ) );\r\n\t\tsplatPage.Layout.Add( FloatSlider( nameof( SplatBlendStrength ) ) );\r\n\t\tsplatPage.Layout.Add( new Label( \"Splatmap Colors/Threshold\" ) );\r\n\t\t_gradientControlWidget = new GradientControlWidget( _serialized.GetProperty( nameof( SplatMapGradient ) ) );\r\n\t\tsplatPage.Layout.Add( _gradientControlWidget );\r\n\t\tsplatPage.Layout.Add( new Label( \"Preview Materials\" ) );\r\n\t\tsplatPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( PreviewSplatMaterials ) ) ) );\r\n\t\tvar materialHint = new Label( \"Assigns random local .tmat terrain materials from your project's assets to the splat layers so you can preview the material blending on the terrain.\" );\r\n\t\tmaterialHint.SetStyles( \"font-size: 10px; color: #888;\" );\r\n\t\tmaterialHint.WordWrap = true;\r\n\t\tmaterialHint.MaximumWidth = 260;\r\n\t\tsplatPage.Layout.Add( materialHint );\r\n\t\tvar randomizeRow = splatPage.Layout.AddRow();\r\n\t\t_randomizeMaterialsButton = randomizeRow.Add( new Button( \"Randomize Materials\", \"casino\" ) );\r\n\t\t_randomizeMaterialsButton.Clicked += RandomizeMaterials;\r\n\t\t_materialLoadingLabel = randomizeRow.Add( new Label( \"Loading...\" ) );\r\n\t\t_materialLoadingLabel.SetStyles( \"font-size: 10px; color: #888;\" );\r\n\t\t_materialLoadingLabel.Visible = false;\r\n\t\t_materialLoadingLabel.WordWrap = true;\r\n\t\t_materialLoadingLabel.MaximumWidth = 120;\r\n\t\tAddPropsTab( \"Splat\", \"palette\", splatPage, \"Splatmap layers, maps, colors and dispersion\" );\r\n\r\n\t\t// --- Warping tab ---\r\n\t\tvar warpPage = CreatePropsPage();\r\n\r\n\t\twarpPage.Layout.Add( new Label( \"Domain Warping\" ) );\r\n\t\twarpPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( DomainWarping ) ) ) );\r\n\t\tvar DomainWarpingSizeLabel = warpPage.Layout.Add( new Label( \"Domain Warping (Size)\" ) );\r\n\t\tvar DomainWarpingSizeFloat = warpPage.Layout.Add( FloatSlider( nameof( DomainWarpingSize ) ) );\r\n\t\tvar DomainWarpingStrengthLabel = warpPage.Layout.Add( new Label( \"Domain Warping (Strength)\" ) );\r\n\t\tvar DomainWarpingStrengthFloat = warpPage.Layout.Add( FloatSlider( nameof( DomainWarpingStrength ) ) );\r\n\t\t_domainWarpingWidgets.AddRange( new Widget[] { DomainWarpingSizeLabel, DomainWarpingSizeFloat, DomainWarpingStrengthLabel, DomainWarpingStrengthFloat } );\r\n\t\tAddPropsTab( \"Warping\", \"blur_on\", warpPage, \"Domain warping options\" );\r\n\r\n\t\t// --- River tab ---\r\n\t\tvar riverPage = CreatePropsPage();\r\n\r\n\t\triverPage.Layout.Add( new Label( \"River Carving\" ) );\r\n\t\triverPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( RiverCarvingBool ) ) ) );\r\n\t\tvar RiverCarvingFrequencyLabel = riverPage.Layout.Add( new Label( \"RiverCarvingFrequency\" ) );\r\n\t\tvar RiverCarvingFrequencyFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingFrequency ) ) );\r\n\t\t/*var RiverCarvingStrength = riverPage.Layout.Add( new Label(\"RiverCarvingStrength\"));\r\n\t\tvar RiverCarvingStrengthFloat = riverPage.Layout.Add( FloatSlider( nameof(RiverCarvingStrength) ) );*/\r\n\t\tvar RiverCarvingDepthLabel = riverPage.Layout.Add( new Label( \"RiverCarvingDepth\" ) );\r\n\t\tvar RiverCarvingDepthFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingDepth ) ) );\r\n\t\tvar RiverCarvingWidthLabel = riverPage.Layout.Add( new Label( \"RiverCarvingWidth\" ) );\r\n\t\tvar RiverCarvingWidthFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingWidth ) ) );\r\n\t\tvar RiverCarvingSpacingLabel = riverPage.Layout.Add( new Label( \"RiverCarvingSpacing\" ) );\r\n\t\tvar RiverCarvingSpacingFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingSpacing ) ) );\r\n\t\tvar RiverCarvingTurbulenceStrengthLabel = riverPage.Layout.Add( new Label( \"RiverCarvingTurbulenceStrength\" ) );\r\n\t\tvar RiverCarvingTurbulenceStrengthFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingTurbulenceStrength ) ) );\r\n\t\tvar RiverCarvingTurbulenceFrequencyLabel = riverPage.Layout.Add( new Label( \"RiverCarvingTurbulenceFrequency\" ) );\r\n\t\tvar RiverCarvingTurbulenceFrequencyFloat = riverPage.Layout.Add( FloatSlider( nameof( RiverCarvingTurbulenceFrequency ) ) );\r\n\t\t_riverCarvingWidgets.AddRange( new Widget[]\r\n\t\t{\r\n\t\t\tRiverCarvingFrequencyLabel, RiverCarvingFrequencyFloat,\r\n\t\t\t/*RiverCarvingStrength, RiverCarvingStrengthFloat,*/\r\n\t\t\tRiverCarvingDepthLabel, RiverCarvingDepthFloat,\r\n\t\t\tRiverCarvingWidthLabel, RiverCarvingWidthFloat,\r\n\t\t\tRiverCarvingSpacingLabel, RiverCarvingSpacingFloat,\r\n\t\t\tRiverCarvingTurbulenceStrengthLabel, RiverCarvingTurbulenceStrengthFloat,\r\n\t\t\tRiverCarvingTurbulenceFrequencyLabel, RiverCarvingTurbulenceFrequencyFloat\r\n\t\t} );\r\n\t\tAddPropsTab( \"River\", \"water\", riverPage, \"River carving options\" );\r\n\r\n\t\t// --- Staging tab ---\r\n\t\tvar stagingPage = CreatePropsPage();\r\n\r\n\t\tstagingPage.Layout.Add( new Label( \"Staging Area\" ) );\r\n\t\tstagingPage.Layout.Add( new BoolControlWidget( _serialized.GetProperty( nameof( StagingArea ) ) ) );\r\n\t\tvar StagingAreaSizeLabel = stagingPage.Layout.Add( new Label( \"Staging Area (Size)\" ) );\r\n\t\tvar StagingAreaSizeFloat = stagingPage.Layout.Add( IntSlider( nameof( StagingAreaSize ) ) );\r\n\t\tvar StagingAreaHeightLabel = stagingPage.Layout.Add( new Label( \"Staging Area (Height)\" ) );\r\n\t\tvar StagingAreaHeightFloat = stagingPage.Layout.Add( FloatSlider( nameof( StagingAreaHeight ) ) );\r\n\t\tvar StagingAreaXLabel = stagingPage.Layout.Add( new Label( \"Staging Area (X)\" ) );\r\n\t\tvar StagingAreaXFloat = stagingPage.Layout.Add( FloatSlider( nameof( StagingAreaX ) ) );\r\n\t\tvar StagingAreaYLabel = stagingPage.Layout.Add( new Label( \"Staging Area (Y)\" ) );\r\n\t\tvar StagingAreaYFloat = stagingPage.Layout.Add( FloatSlider( nameof( StagingAreaY ) ) );\r\n\t\t_stagingAreaWidgets.AddRange( new Widget[]\r\n\t\t{\r\n\t\t\tStagingAreaSizeLabel, StagingAreaSizeFloat,\r\n\t\t\tStagingAreaHeightLabel, StagingAreaHeightFloat,\r\n\t\t\tStagingAreaXLabel, StagingAreaXFloat,\r\n\t\t\tStagingAreaYLabel, StagingAreaYFloat\r\n\t\t} );\r\n\t\tAddPropsTab( \"Staging\", \"square_foot\", stagingPage, \"Staging area placement\" );\r\n\r\n\t\tbody.AddSpacingCell( 5 );\r\n\r\n\t\t// ---------- Tile grid section (docked between props and actions) ----------\r\n\t\tvar tileGridSection = body.Add( new Widget( null ) );\r\n\t\ttileGridSection.Layout = Layout.Column();\r\n\t\ttileGridSection.Layout.Spacing = 4;\r\n\r\n\t\ttileGridSection.Layout.Add( new Label( \"Tile Grid\" ) );\r\n\t\ttileGridSection.Layout.Add( IntSlider( nameof( TerrainGridSize ) ) );\r\n\t\ttileGridSection.Layout.Add( new Label( \"Storage\" ) );\r\n\t\ttileGridSection.Layout.Add( new EnumControlWidget( _serialized.GetProperty( nameof( GridStorage ) ) ) );\r\n\t\tvar tileGridHint = new Label( \"Click a tile to select which cell the Terrain Type category and shape apply to. Click 'All' to set every tile at once.\" );\r\n\t\ttileGridHint.SetStyles( \"font-size: 10px; color: #888;\" );\r\n\t\ttileGridHint.WordWrap = true;\r\n\t\ttileGridHint.MaximumWidth = 260;\r\n\t\ttileGridSection.Layout.Add( tileGridHint );\r\n\r\n\t\t_tilesContainer = new Widget( null );\r\n\t\t_tilesContainer.Layout = Layout.Column();\r\n\t\t_tilesContainer.Layout.Spacing = 4;\r\n\t\ttileGridSection.Layout.Add( _tilesContainer );\r\n\r\n\t\tbody.AddSpacingCell( 5 );\r\n\r\n\t\tvar GenerateButton = body.Add( new Button.Primary( \"Generate\", \"auto_awesome\", this ) );\r\n\r\n\t\tvar ExportButton = body.Add( new Button( \"Export\", \"file_download\", this ) );\r\n\t\tExportButton.Tint = \"#41AF20\";\r\n\r\n\t\tvar ApplyButton = body.Add( new Button( \"Apply To Terrain\", \"file_upload\", this ) );\r\n\t\tApplyButton.Tint = \"#AF2020\";\r\n\r\n\t\tif ( _heightmap == null )\r\n\t\t{\r\n\t\t\tExportButton.Enabled = false;\r\n\t\t\tApplyButton.Enabled = false;\r\n\r\n\t\t}\r\n\r\n\t\tGenerateButton.Clicked += () =>\r\n\t\t{\r\n\t\t\tBuildSplatColors();\r\n\r\n\t\t\tint fullRes = (int)TerrainDimensionsEnum;\r\n\r\n\t\t\tif ( GridStorage == GridStorageMode.PerCell )\r\n\t\t\t{\r\n\t\t\t\t// Each cell is its own full-resolution map - no stitching.\r\n\t\t\t\t_cellHeightmaps = BuildPerCellHeightmaps(\r\n\t\t\t\t\tfullRes, fullRes,\r\n\t\t\t\t\t(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(),\r\n\t\t\t\t\t(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),\r\n\t\t\t\t\t(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),\r\n\t\t\t\t\t(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),\r\n\t\t\t\t\t(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),\r\n\t\t\t\t\tRiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,\r\n\t\t\t\t\tRiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,\r\n\t\t\t\t\tStagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );\r\n\r\n\t\t\t\tif ( _cellHeightmaps.Count == 0 )\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Error( \"No per-cell heightmaps generated. Aborting.\" );\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\t_cellSplatmaps = BuildPerCellSplatmaps( _cellHeightmaps,\r\n\t\t\t\t\t(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );\r\n\r\n\t\t\t\t// A stitched preview map so the 3D preview still shows the whole grid tiled.\r\n\t\t\t\t_heightmap = BuildHeightmap(\r\n\t\t\t\t\tfullRes, fullRes,\r\n\t\t\t\t\t(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(), TerrainGridSize,\r\n\t\t\t\t\t(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),\r\n\t\t\t\t\t(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),\r\n\t\t\t\t\t(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),\r\n\t\t\t\t\t(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),\r\n\t\t\t\t\tRiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,\r\n\t\t\t\t\tRiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,\r\n\t\t\t\t\tStagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_heightmap = BuildHeightmap(\r\n\t\t\t\t\tfullRes, fullRes,\r\n\t\t\t\t\t(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(), TerrainGridSize,\r\n\t\t\t\t\t(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),\r\n\t\t\t\t\t(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),\r\n\t\t\t\t\t(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),\r\n\t\t\t\t\t(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),\r\n\t\t\t\t\tRiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,\r\n\t\t\t\t\tRiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,\r\n\t\t\t\t\tStagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );\r\n\r\n\t\t\t\t_cellHeightmaps = null;\r\n\t\t\t\t_cellSplatmaps = null;\r\n\t\t\t}\r\n\r\n\t\t\tif ( _heightmap == null )\r\n\t\t\t{\r\n\t\t\t\tLog.Error( \"Heightmap is not generated. Aborting.\" );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\t_splatmap = BuildTileGridSplatmap( _heightmap, TerrainGridSize,\r\n\t\t\t\t(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );\r\n\r\n\t\t\t// Write the preview files for the asset folder, and build fresh textures for the widgets\r\n\t\t\tGeneratePreviewFile( GenerationPath, out var previewBitmap, out var splatBitmap );\r\n\r\n\t\t\t_preview_image_texture = TextureFromBitmap( previewBitmap );\r\n\t\t\tPreviewImage.Texture = _preview_image_texture;\r\n\r\n\t\t\t_preview_splatmap_texture = TextureFromBitmap( splatBitmap );\r\n\t\t\tPreviewSplatmap.Texture = _preview_splatmap_texture;\r\n\r\n\t\t\tExportButton.Enabled = true;\r\n\t\t\tApplyButton.Enabled = true;\r\n\r\n\t\t\tUpdatePreviewTerrain( _heightmap );\r\n\r\n\t\t};\r\n\r\n\t\tExportButton.Clicked += () =>\r\n\t\t{\r\n\t\t\tGenerateImageFiles( ExportPath );\r\n\t\t\tvar PopUp = new PopupWindow( \"Export Complete\", $\"Files exported to {ExportPath}\", \"Okay\" );\r\n\t\t\tPopUp.Show();\r\n\t\t};\r\n\r\n\t\tApplyButton.Clicked += () =>\r\n\t\t{\r\n\t\t\tIDictionary<string, Action> WarnDiaglog = new Dictionary<string, Action>(); ;\r\n\t\t\tWarnDiaglog.Add( \"Apply\", GridStorage == GridStorageMode.PerCell ? UpdatePerCellTerrains : UpdateTerrain );\r\n\t\t\tvar PopUpWarn = new PopupWindow( \"Warning: Terrain Override\", \"This will override your current scene's terrain data.\",\"Cancel\", WarnDiaglog );\r\n\t\t\tPopUpWarn.Show();\r\n\t\t};\r\n\t\t\r\n\t\tbody.AddStretchCell();\r\n\r\n\t\t// ---------- Right: tabbed preview panel ----------\r\n\t\tvar rightPanel = Layout.Add( new Widget( null ), 1 );\r\n\t\trightPanel.Layout = Layout.Column();\r\n\t\trightPanel.Layout.Spacing = 0;\r\n\r\n\t\tvar PreviewTabs = rightPanel.Layout.Add( new VerticalTabWidget( this ), 1 );\r\n\t\tPreviewTabs.StateCookie = \"TerrainGenerationTool.PreviewTabs\";\r\n\r\n\t\tRenderCanvas = new SceneRenderingWidget( this );\r\n\t\tRenderCanvas.OnPreFrame += OnPreFrame;\r\n\t\tRenderCanvas.FocusMode = FocusMode.Click;\r\n\t\tRenderCanvas.Scene = Scene.CreateEditorScene();\r\n\t\tRenderCanvas.Scene.SceneWorld.AmbientLightColor = Color.FromBytes( 135, 206, 235 ) * 0.45f;\r\n\r\n\t\t// 3D preview tab\r\n\t\tPreviewTabs.AddPage( \"3D Preview\", \"landscape\", RenderCanvas, \"3D terrain preview\" );\r\n\r\n\t\t// Height/Color maps tab\r\n\t\tvar mapsPage = new Widget( null );\r\n\t\tmapsPage.Layout = Layout.Row();\r\n\t\tmapsPage.Layout.Spacing = 5;\r\n\r\n\t\tvar _image_preview = new Editor.TextureWidget();\r\n\t\t_image_preview.Texture = _preview_image_texture;\r\n\t\t_image_preview.Size = new Vector2( 512, 512 );\r\n\t\tPreviewImage = mapsPage.Layout.Add( _image_preview, 50 );\r\n\r\n\t\tvar _splatmap_preview = new Editor.TextureWidget();\r\n\t\t_splatmap_preview.Texture = _preview_splatmap_texture;\r\n\t\t_splatmap_preview.Size = new Vector2( 512, 512 );\r\n\t\tPreviewSplatmap = mapsPage.Layout.Add( _splatmap_preview, 50 );\r\n\r\n\t\tPreviewTabs.AddPage( \"Height/Color Maps\", \"grid_view\", mapsPage, \"Heightmap and splatmap preview\" );\r\n\r\n\t\tusing ( RenderCanvas.Scene.Push() )\r\n\t\t{\r\n\t\t\tCamera = new GameObject( true, \"camera\" ).GetOrAddComponent<CameraComponent>( false );\r\n\t\t\tCamera.BackgroundColor = Color.FromBytes( 135, 206, 235 );\r\n\t\t\tCamera.ZFar = 100000;\r\n\t\t\tCamera.Enabled = true;\r\n\t\t\tPositionCameraForOrbit();\r\n\t\t\tRenderCanvas.Camera = Camera;\r\n\r\n\t\t\tvar sun = new GameObject( true, \"sun\" ).GetOrAddComponent<DirectionalLight>( false );\r\n\t\t\tsun.WorldRotation = Rotation.From( 45, 45, 0 );\r\n\t\t\tsun.LightColor = Color.White;\r\n\t\t\tsun.SkyColor = Color.FromBytes( 135, 206, 235 );\r\n\t\t\tsun.Enabled = true;\r\n\r\n\t\t\tvar sun2 = new GameObject( true, \"sun2\" ).GetOrAddComponent<DirectionalLight>( false );\r\n\t\t\tsun2.WorldRotation = Rotation.From( -30, 135, 0 );\r\n\t\t\tsun2.LightColor = Color.White * 0.3f;\r\n\t\t\tsun2.SkyColor = Color.FromBytes( 135, 206, 235 );\r\n\t\t\tsun2.Enabled = true;\r\n\t\t}\r\n\r\n\t\tGizmoInstance = RenderCanvas.GizmoInstance;\r\n\r\n\t\t// Create the preview terrain - a real Terrain component in the preview scene\r\n\t\tusing ( RenderCanvas.Scene.Push() )\r\n\t\t{\r\n\t\t\t_previewGO = new GameObject( true, \"terrain preview\" );\r\n\t\t\t_previewTerrain = _previewGO.AddComponent<Terrain>( false );\r\n\t\t\t_previewStorage = new TerrainStorage();\r\n\t\t\t_previewStorage.EmbeddedResource = new Sandbox.Resources.EmbeddedResource { ResourceCompiler = \"embed\" };\r\n\t\t\t_previewStorage.SetResolution( PreviewResolution );\r\n\t\t\t_previewStorage.TerrainSize = PreviewTerrainSize;\r\n\t\t\t_previewStorage.TerrainHeight = PreviewTerrainHeight;\r\n\t\t\t_previewTerrain.Storage = _previewStorage;\r\n\t\t\t_previewTerrain.TerrainSize = PreviewTerrainSize;\r\n\t\t\t_previewTerrain.TerrainHeight = PreviewTerrainHeight;\r\n\t\t\t// Terrain spans [0, TerrainSize] from its origin - shift it so it's centered on the origin\r\n\t\t\t_previewGO.WorldPosition = new Vector3( -PreviewTerrainSize * 0.5f, -PreviewTerrainSize * 0.5f, 0f );\r\n\t\t\t_previewTerrain.Enabled = true;\r\n\r\n\t\t\t// Overlay mesh that shows the splatmap colors when the material preview is off.\r\n\t\t\t// Slightly offset above the terrain so it doesn't z-fight with the terrain surface.\r\n\t\t\t// Parented to the terrain GO (which is centered at origin), so the mesh uses local coords.\r\n\t\t\t_splatOverlayGO = new GameObject( true, \"splat overlay\" );\r\n\t\t\t_splatOverlayGO.Parent = _previewGO;\r\n\t\t\t_splatOverlayGO.LocalPosition = new Vector3( 0, 0, 1f );\r\n\t\t\t_splatOverlayRenderer = _splatOverlayGO.AddComponent<ModelRenderer>();\r\n\t\t\t_splatOverlayRenderer.MaterialOverride = Material.Load( \"materials/default/vertex_color.vmat\" );\r\n\t\t\t_splatOverlayGO.Enabled = false;\r\n\t\t}\r\n\r\n\t\t// Zoom slider at the bottom of the preview panel\r\n\t\tvar zoomRow = rightPanel.Layout.AddRow();\r\n\t\tzoomRow.Margin = new Sandbox.UI.Margin( 8, 4, 8, 6 );\r\n\t\tzoomRow.Spacing = 8;\r\n\r\n\t\tzoomRow.Add( new IconButton( \"zoom_out\", () => ZoomSlider.Value = MathF.Max( ZoomSlider.Minimum, ZoomSlider.Value - 500f ), this ) { IconSize = 16, FixedSize = new Vector2( 22, 22 ) } );\r\n\t\tZoomSlider = zoomRow.Add( new FloatSlider( this ), 1 );\r\n\t\tZoomSlider.Minimum = 10000f;\r\n\t\tZoomSlider.Maximum = 40000f;\r\n\t\tZoomSlider.Step = 500f;\r\n\t\tZoomSlider.Value = 40000f - _orbitDistance + 10000f;\r\n\t\tZoomSlider.OnValueEdited = UpdateOrbitFromZoom;\r\n\t\tzoomRow.Add( new IconButton( \"zoom_in\", () => ZoomSlider.Value = MathF.Min( ZoomSlider.Maximum, ZoomSlider.Value + 500f ), this ) { IconSize = 16, FixedSize = new Vector2( 22, 22 ) } );\r\n\r\n\t\tApplyConditionalVisibility();\r\n\t\tRebuildTileGridUI();\r\n\t\tLoadFacepunchMaterialsAsync();\r\n\t\tRegeneratePreview();\r\n\t\tShow();\r\n\t}\r\n\r\n\tvoid OnSerializedPropertyChanged( SerializedProperty prop )\r\n\t{\r\n\t\t// Intermediate frames written during gradient animation shouldn't retrigger a regen\r\n\t\tif ( !_isAnimatingGradient )\r\n\t\t\t_previewDirty = true;\r\n\r\n\t\tif ( prop is null ) return;\r\n\r\n\t\tswitch ( prop.Name )\r\n\t\t{\r\n\t\t\tcase nameof( TerrainGridSize ):\r\n\t\t\t\tRebuildTileGridUI();\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( GridStorage ):\r\n\t\t\t\t// Reset stored per-cell maps when the storage mode changes so stale data isn't applied/exported\r\n\t\t\t\t_cellHeightmaps = null;\r\n\t\t\t\t_cellSplatmaps = null;\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( TerrainMinHeight ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( minHeight: TerrainMinHeight );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( TerrainMaxHeight ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( maxHeight: TerrainMaxHeight );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( TerrainPlaneScale ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( planeScale: TerrainPlaneScale );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( TerrainSeed ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( seed: TerrainSeed );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( SmoothingPasses ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( smoothing: SmoothingPasses );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( NoiseLayerStacks ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( noiseLayers: NoiseLayerStacks );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( DomainWarping ):\r\n\t\t\t\tSetWidgetsVisible( _domainWarpingWidgets, DomainWarping );\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( warp: DomainWarping );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( DomainWarpingSize ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( warpSize: DomainWarpingSize );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( DomainWarpingStrength ):\r\n\t\t\t\tif ( !_syncingTileSelectors ) WriteSelectedValues( warpStrength: DomainWarpingStrength );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( StagingArea ):\n\t\t\t\tSetWidgetsVisible( _stagingAreaWidgets, StagingArea );\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( SplatLayerCount ):\r\n\t\t\tcase nameof( SplatDispersion ):\r\n\t\t\tcase nameof( SplatBlendStrength ):\r\n\t\t\tcase nameof( SplatMapCount ):\r\n\t\t\t\tif ( !_syncingTileSelectors )\r\n\t\t\t\t{\r\n\t\t\t\t\tWriteSelectedValues(\r\n\t\t\t\t\t\tsplatLayers: prop.Name == nameof( SplatLayerCount ) ? SplatLayerCount : (int?)null,\r\n\t\t\t\t\t\tsplatMaps: prop.Name == nameof( SplatMapCount ) ? SplatMapCount : (int?)null,\r\n\t\t\t\t\t\tsplatDispersion: prop.Name == nameof( SplatDispersion ) ? SplatDispersion : (SplatDispersionMode?)null,\r\n\t\t\t\t\t\tsplatBlend: prop.Name == nameof( SplatBlendStrength ) ? SplatBlendStrength : (float?)null );\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// Resample the gradient into evenly spaced stops so the colors/thresholds match the layer count\r\n\t\t\t\tResampleSplatGradient();\r\n\t\t\t\tif ( PreviewSplatMaterials )\r\n\t\t\t\t{\r\n\t\t\t\t\t_previewMaterials = null;\r\n\t\t\t\t\tRandomizeMaterialsAsync();\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( SplatMapGradient ):\r\n\t\t\t\t// The user edited the gradient colors in the widget - make that the source of\r\n\t\t\t\t// truth so later resamples keep their colors instead of falling back to the\r\n\t\t\t\t// stale random cache.\r\n\t\t\t\tif ( !_isAnimatingGradient )\r\n\t\t\t\t{\r\n\t\t\t\t\t_splatColorCache.Clear();\r\n\t\t\t\t\tif ( SplatMapGradient.Colors != null )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tforeach ( var frame in SplatMapGradient.Colors )\r\n\t\t\t\t\t\t\t_splatColorCache.Add( frame.Value );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t\tcase nameof( PreviewSplatMaterials ):\r\n\t\t\t\tif ( PreviewSplatMaterials && ( _previewMaterials == null || _previewMaterials.Length < Math.Max( SplatLayerCount, 2 ) ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tRandomizeMaterials();\r\n\t\t\t\t}\r\n\t\t\t\tif ( !PreviewSplatMaterials )\r\n\t\t\t\t{\r\n\t\t\t\t\t_previewMaterials = null;\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// The largest splat layer count across all tiles (so shared resources like the preview\r\n\t/// material list and gradient cover every tile). Falls back to the global value.\r\n\t/// </summary>\r\n\tint MaxTileSplatLayers()\r\n\t{\r\n\t\tint max = Math.Max( SplatLayerCount, 2 );\r\n\t\tif ( _tileSplatLayerCounts != null )\r\n\t\t{\r\n\t\t\tforeach ( var lc in _tileSplatLayerCounts )\r\n\t\t\t\tmax = Math.Max( max, lc );\r\n\t\t}\r\n\t\treturn max;\r\n\t}\r\n\r\n\tvoid ResampleSplatGradient()\r\n\t{\r\n\t\tint layerCount = MaxTileSplatLayers();\r\n\r\n\t\t// Seed the persistent color cache from the current gradient first so user edits are kept.\r\n\t\tif ( _splatColorCache.Count == 0 && SplatMapGradient.Colors != null && SplatMapGradient.Colors.Count() > 0 )\r\n\t\t{\r\n\t\t\tforeach ( var frame in SplatMapGradient.Colors )\r\n\t\t\t\t_splatColorCache.Add( frame.Value );\r\n\t\t}\r\n\r\n\t\t// Add new colors to the end as layers grow - existing colors keep their index.\r\n\t\twhile ( _splatColorCache.Count < layerCount )\r\n\t\t\t_splatColorCache.Add( RandomBrightColor() );\r\n\r\n\t\t// Compute the target stop positions.\r\n\t\tfloat[] thresholds;\r\n\t\tvar source = _previewHeightmap ?? _heightmap;\r\n\t\tif ( SplatDispersion == SplatDispersionMode.Natural && source != null )\r\n\t\t{\r\n\t\t\tthresholds = ComputeNaturalThresholds( source, layerCount );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tthresholds = new float[layerCount];\r\n\t\t\tfor ( int i = 0; i < layerCount; i++ )\r\n\t\t\t{\r\n\t\t\t\tthresholds[i] = (float)i / (layerCount - 1);\r\n\t\t\t}\r\n\t\t}\r\n\t\t_splatthresholds = thresholds;\r\n\r\n\t\t// First build: set the gradient directly.\r\n\t\tif ( _currentFrameTimes is null )\r\n\t\t{\r\n\t\t\t_currentFrameTimes = thresholds.ToList();\r\n\t\t\t_targetFrameTimes = thresholds.ToList();\r\n\t\t\tApplyGradientFromFrames();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tint oldCount = _currentFrameTimes.Count;\r\n\r\n\t\t// New frames (added layers) enter from the right and slide left to their target.\r\n\t\tif ( layerCount > oldCount )\r\n\t\t{\r\n\t\t\tfor ( int i = oldCount; i < layerCount; i++ )\r\n\t\t\t\t_currentFrameTimes.Add( 1f );\r\n\t\t\t_targetFrameTimes = thresholds.ToList();\r\n\t\t}\r\n\t\t// Removed frames slide out to the right (target 1.0) and get dropped when they arrive.\r\n\t\telse if ( layerCount < oldCount )\r\n\t\t{\r\n\t\t\t// Existing frames keep their current positions; the extra ones head right.\r\n\t\t\tvar newTargets = thresholds.ToList();\r\n\t\t\twhile ( newTargets.Count < oldCount )\r\n\t\t\t\tnewTargets.Add( 1f );\r\n\t\t\t_targetFrameTimes = newTargets;\r\n\t\t}\r\n\t\t// Same count - just retarget the existing frames.\r\n\t\telse\r\n\t\t{\r\n\t\t\t_targetFrameTimes = thresholds.ToList();\r\n\t\t}\r\n\r\n\t\t_gradientAnimating = true;\r\n\t}\r\n\r\n\tvoid ApplyGradientFromFrames()\r\n\t{\r\n\t\t// Only include frames that are still \"in play\" (haven't slid off the right edge yet).\r\n\t\tvar frames = new List<Gradient.ColorFrame>();\r\n\t\tfor ( int i = 0; i < _currentFrameTimes.Count && i < _splatColorCache.Count; i++ )\r\n\t\t{\r\n\t\t\tfloat t = Math.Clamp( _currentFrameTimes[i], 0f, 1f );\r\n\t\t\tframes.Add( new Gradient.ColorFrame( t, _splatColorCache[i] ) );\r\n\t\t}\r\n\r\n\t\tSplatMapGradient = new Gradient( frames.ToArray() );\r\n\t\tSplatMapGradient.Blending = Gradient.BlendMode.Stepped;\r\n\r\n\t\t_isAnimatingGradient = true;\r\n\t\ttry\r\n\t\t{\r\n\t\t\t_serialized.GetProperty( nameof( SplatMapGradient ) )?.SetValue( SplatMapGradient );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_isAnimatingGradient = false;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Eases the gradient color stops toward their target positions so palette changes\r\n\t/// slide in/out on the scale instead of snapping.\r\n\t/// </summary>\r\n\tvoid UpdateGradientAnimation()\r\n\t{\r\n\t\tif ( !_gradientAnimating || _currentFrameTimes is null || _targetFrameTimes is null ) return;\r\n\r\n\t\tfloat t = 1f - MathF.Exp( -PreviewMorphSpeed * RealTime.Delta );\r\n\r\n\t\tfloat maxDelta = 0f;\r\n\t\tfor ( int i = 0; i < _currentFrameTimes.Count && i < _targetFrameTimes.Count; i++ )\r\n\t\t{\r\n\t\t\tfloat delta = _targetFrameTimes[i] - _currentFrameTimes[i];\r\n\t\t\t_currentFrameTimes[i] += delta * t;\r\n\t\t\tmaxDelta = MathF.Max( maxDelta, MathF.Abs( delta ) );\r\n\t\t}\r\n\r\n\t\t// Drop frames that have slid off the right edge (removed layers)\r\n\t\tif ( _currentFrameTimes.Count > _targetFrameTimes.Count )\r\n\t\t{\r\n\t\t\twhile ( _currentFrameTimes.Count > _targetFrameTimes.Count )\r\n\t\t\t{\r\n\t\t\t\tint last = _currentFrameTimes.Count - 1;\r\n\t\t\t\tif ( _currentFrameTimes[last] >= 0.999f )\r\n\t\t\t\t{\r\n\t\t\t\t\t_currentFrameTimes.RemoveAt( last );\r\n\t\t\t\t\tif ( _splatColorCache.Count > _targetFrameTimes.Count )\r\n\t\t\t\t\t\t_splatColorCache.RemoveAt( _splatColorCache.Count - 1 );\r\n\t\t\t\t}\r\n\t\t\t\telse break;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tApplyGradientFromFrames();\r\n\r\n\t\t// Force the gradient widget to repaint this frame so the motion is smooth\r\n\t\tif ( _gradientControlWidget != null && _gradientControlWidget.IsValid() )\r\n\t\t\t_gradientControlWidget.Update();\r\n\r\n\t\t// The overlay mesh samples the live gradient, so rebuild it while the palette animates\r\n\t\tBuildOverlayMesh();\r\n\r\n\t\tif ( maxDelta < 0.001f )\r\n\t\t{\r\n\t\t\t_currentFrameTimes = _targetFrameTimes.ToList();\r\n\t\t\t_gradientAnimating = false;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic Color RandomBrightColor()\r\n\t{\r\n\t\t// Pick a hue at random, keep saturation/value high so it stands out\r\n\t\tfloat hue = Random.Shared.NextSingle() * 360f;\r\n\t\treturn new ColorHsv( hue, 0.8f, 1.0f ).ToColor();\r\n\t}\r\n\r\n\tvoid SetWidgetsVisible( List<Widget> widgets, bool visible )\r\n\t{\r\n\t\tforeach ( var widget in widgets )\r\n\t\t{\r\n\t\t\twidget.Visible = visible;\r\n\t\t\twidget.Enabled = visible;\r\n\t\t}\r\n\t}\r\n\r\n\tWidget CreatePropsPage()\r\n\t{\r\n\t\tvar page = new Widget( null );\r\n\t\tpage.VerticalSizeMode = SizeMode.CanShrink;\r\n\t\tpage.HorizontalSizeMode = SizeMode.Flexible;\r\n\t\tpage.Layout = Layout.Column();\r\n\t\tpage.Layout.Margin = 10;\r\n\t\tpage.Layout.Spacing = 5;\r\n\t\tpage.Layout.Alignment = TextFlag.Top;\r\n\t\treturn page;\r\n\t}\r\n\r\n\tFloatControlWidget FloatSlider( string propertyName )\r\n\t{\r\n\t\tvar property = _serialized.GetProperty( propertyName );\r\n\t\tvar control = new FloatControlWidget( property );\r\n\t\tMakeRanged( control, property );\r\n\t\treturn control;\r\n\t}\r\n\r\n\tIntegerControlWidget IntSlider( string propertyName )\r\n\t{\r\n\t\tvar property = _serialized.GetProperty( propertyName );\r\n\t\tvar control = new IntegerControlWidget( property );\r\n\t\tMakeRanged( control, property );\r\n\t\treturn control;\r\n\t}\r\n\r\n\tvoid MakeRanged( FloatControlWidget control, SerializedProperty property )\r\n\t{\r\n\t\tif ( property is null ) return;\r\n\r\n\t\tproperty.TryGetAttribute<MinMaxAttribute>( out var minMax );\r\n\t\tif ( minMax is null ) return;\r\n\r\n\t\tfloat step = 0.01f;\r\n\t\tif ( property.TryGetAttribute<StepAttribute>( out var stepAttr ) )\r\n\t\t{\r\n\t\t\tstep = stepAttr.Step;\r\n\t\t}\r\n\r\n\t\tcontrol.MakeRanged( new Vector2( minMax.MinValue, minMax.MaxValue ), step, true, true );\r\n\t}\r\n\r\n\tvoid RandomizeSeed()\r\n\t{\r\n\t\tvar property = _serialized.GetProperty( nameof( TerrainSeed ) );\r\n\t\tif ( property is null ) return;\r\n\r\n\t\tTerrainSeed = Random.Shared.NextInt64();\r\n\t\tproperty.SetValue( TerrainSeed );\r\n\t\t_previewDirty = true;\r\n\t}\r\n\r\n\tvoid LoadFacepunchMaterialsAsync()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// Find all local tmat assets in the project's assets folder (not cloud ones).\r\n\t\t\t// Prefer 1K variants when a material has multiple resolutions.\r\n\t\t\tvar allLocal = Editor.AssetSystem.All\r\n\t\t\t\t.Where( a => a is not null && !a.IsDeleted && !a.IsCloud )\r\n\t\t\t\t.Where( a => (a.RelativePath?.EndsWith( \".tmat\" ) ?? false) )\r\n\t\t\t\t.ToList();\r\n\r\n\t\t\tvar with1k = allLocal.Where( a => a.RelativePath.Contains( \"_1k\" ) ).ToList();\r\n\r\n\t\t\t_localTmatAssets = with1k.Count > 0 ? with1k : allLocal;\r\n\r\n\t\t\tif ( _localTmatAssets.Count == 0 )\r\n\t\t\t\tLog.Warning( \"No local .tmat terrain materials found in the project's assets folder\" );\r\n\t\t}\r\n\t\tcatch ( System.Exception e )\r\n\t\t{\r\n\t\t\tLog.Error( $\"Failed to find local terrain materials: {e.Message}\" );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid RandomizeMaterials()\r\n\t{\r\n\t\tif ( !PreviewSplatMaterials ) return;\r\n\r\n\t\tif ( _localTmatAssets is null || _localTmatAssets.Count == 0 )\r\n\t\t{\r\n\t\t\t// Load the local tmat list first, then randomize once it's available\r\n\t\t\t_ = LoadFacepunchMaterialsAndRandomize();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tRandomizeMaterialsAsync();\r\n\t}\r\n\r\n\tasync Task LoadFacepunchMaterialsAndRandomize()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// Find all local tmat assets in the project's assets folder (not cloud ones).\r\n\t\t\t// Prefer 1K variants when a material has multiple resolutions.\r\n\t\t\tvar allLocal = Editor.AssetSystem.All\r\n\t\t\t\t.Where( a => a is not null && !a.IsDeleted && !a.IsCloud )\r\n\t\t\t\t.Where( a => (a.RelativePath?.EndsWith( \".tmat\" ) ?? false) )\r\n\t\t\t\t.ToList();\r\n\r\n\t\t\tvar with1k = allLocal.Where( a => a.RelativePath.Contains( \"_1k\" ) ).ToList();\r\n\r\n\t\t\t_localTmatAssets = with1k.Count > 0 ? with1k : allLocal;\r\n\r\n\t\t\tif ( PreviewSplatMaterials && _localTmatAssets.Count > 0 )\r\n\t\t\t\tRandomizeMaterialsAsync();\r\n\t\t}\r\n\t\tcatch ( System.Exception e )\r\n\t\t{\r\n\t\t\tLog.Error( $\"Failed to find local terrain materials: {e.Message}\" );\r\n\t\t}\r\n\t}\r\n\r\n\tasync void RandomizeMaterialsAsync()\r\n\t{\r\n\t\tif ( _materialLoadingLabel != null )\r\n\t\t{\r\n\t\t\t_materialLoadingLabel.Text = \"Loading materials...\";\r\n\t\t\t_materialLoadingLabel.Visible = true;\r\n\t\t}\r\n\r\n\t\tint layerCount = MaxTileSplatLayers();\r\n\t\tint gen = ++_previewMaterialsGeneration;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar pool = new List<Editor.Asset>( _localTmatAssets );\r\n\r\n\t\t\tvar materials = new List<TerrainMaterial>();\r\n\r\n\t\t\t// Pull from the pool until we have layerCount usable materials (skipping any\r\n\t\t\t// whose textures fail to compile) or the pool runs out.\r\n\t\t\twhile ( materials.Count < layerCount && pool.Count > 0 )\r\n\t\t\t{\r\n\t\t\t\tint idx = Random.Shared.Next( pool.Count );\r\n\t\t\t\tvar asset = pool[idx];\r\n\t\t\t\tpool.RemoveAt( idx );\r\n\r\n\t\t\t\tif ( !asset.TryLoadResource<TerrainMaterial>( out var found ) || found is null )\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Warning( $\"Failed to load TerrainMaterial from '{asset.Path}'\" );\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// Only accept materials whose compiled BCR/NHO textures actually exist -\r\n\t\t\t\t// otherwise the terrain renders a pink checkerboard.\r\n\t\t\t\tif ( !IsMaterialUsable( found, asset.Path ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tmaterials.Add( found );\r\n\t\t\t}\r\n\r\n\t\t\tif ( gen != _previewMaterialsGeneration ) return;\r\n\r\n\t\t\tif ( materials.Count == 0 )\r\n\t\t\t{\r\n\t\t\t\t_previewMaterials = null;\r\n\t\t\t\tLog.Warning( \"No local terrain materials could be loaded\" );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_previewMaterials = materials.ToArray();\r\n\t\t\t}\r\n\r\n\t\t\t_previewDirty = true;\r\n\t\t}\r\n\t\tcatch ( System.Exception e )\r\n\t\t{\r\n\t\t\tLog.Error( $\"Failed to load preview materials: {e.Message}\" );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\tif ( gen == _previewMaterialsGeneration && _materialLoadingLabel != null )\r\n\t\t\t\t_materialLoadingLabel.Visible = false;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Checks that a terrain material's compiled BCR/NHO textures are usable. The terrain shader\r\n\t/// samples these bindlessly, and a missing/failed compile shows up as a pink checkerboard.\r\n\t/// </summary>\r\n\tbool IsMaterialUsable( TerrainMaterial material, string ident )\r\n\t{\r\n\t\tif ( material is null ) return false;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar bcr = material.BCRTexture;\r\n\t\t\tif ( bcr is null || bcr.IsError || !bcr.IsValid )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\"Skipping '{ident}': BCR texture missing or failed to compile\" );\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\r\n\t\t\tvar nho = material.NHOTexture;\r\n\t\t\tif ( nho is null || nho.IsError || !nho.IsValid )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\"Skipping '{ident}': NHO texture missing or failed to compile\" );\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\t\tcatch ( System.Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"Skipping '{ident}': {e.Message}\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid AddPropsTab( string name, string icon, Widget page, string tooltip )\r\n\t{\r\n\t\t_propsTabBar.AddOption( name, icon );\r\n\t\t_propsPages[name] = page;\r\n\t\t_propsContent.Layout.Add( page );\r\n\r\n\t\tpage.Visible = false;\r\n\t\tpage.ToolTip = tooltip;\r\n\r\n\t\tif ( _propsPages.Count == 1 )\r\n\t\t{\r\n\t\t\tSelectPropsTab( name );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid SelectPropsTab( string name )\r\n\t{\r\n\t\tforeach ( var entry in _propsPages )\r\n\t\t{\r\n\t\t\tentry.Value.Visible = entry.Key == name;\r\n\t\t}\r\n\r\n\t\tif ( _activePropsTab != name )\r\n\t\t{\r\n\t\t\t_activePropsTab = name;\r\n\t\t\t_previewDirty = true;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid ApplyConditionalVisibility()\r\n\t{\r\n\t\tSetWidgetsVisible( _domainWarpingWidgets, DomainWarping );\r\n\t\tSetWidgetsVisible( _riverCarvingWidgets, RiverCarvingBool );\r\n\t\tSetWidgetsVisible( _stagingAreaWidgets, StagingArea );\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void FrameUpdate()\r\n\t{\r\n\t\t// Tick the preview scene so the terrain clipmap builds and updates\r\n\t\tif ( RenderCanvas != null && RenderCanvas.Scene.IsValid() )\r\n\t\t\tRenderCanvas.Scene.EditorTick( RealTime.Now, RealTime.Delta );\r\n\r\n\t\t// Morph the overlay mesh toward its target shape every frame\r\n\t\tUpdateOverlayAnimation();\r\n\r\n\t\t// If a tile was just selected/deselected, keep rebuilding the overlay mesh so the\r\n\t\t// grey-out smoothly eases in until the colors settle.\r\n\t\tif ( _overlayColorAnimating )\r\n\t\t{\r\n\t\t\tBuildOverlayMesh();\r\n\t\t}\r\n\r\n\t\t// Animate the gradient color stops sliding in/out\r\n\t\tUpdateGradientAnimation();\r\n\r\n\t\tif ( !_previewDirty ) return;\r\n\t\tif ( RealTime.Now - _lastPreviewRegen < 0.1f ) return;\r\n\t\tif ( _isGenerating ) return;\r\n\r\n\t\t_previewDirty = false;\r\n\t\t_lastPreviewRegen = RealTime.Now;\r\n\r\n\t\tRegeneratePreviewAsync();\r\n\t}\r\n\r\n\tasync void RegeneratePreviewAsync()\r\n\t{\r\n\t\tif ( _previewTerrain is null || !_previewTerrain.IsValid() ) return;\r\n\t\tif ( string.IsNullOrEmpty( CategoryArray?.Selected ) || string.IsNullOrEmpty( ShapeArray?.Selected ) ) return;\r\n\r\n\t\tif ( _isGenerating ) return;\r\n\t\t_isGenerating = true;\r\n\t\tint token = ++_generationToken;\r\n\r\n\t\t// Splat colors are read on the main thread into the shared arrays\r\n\t\tBuildSplatColors();\r\n\r\n\t\t// Snapshot UI-driven values on the main thread so the background task doesn't touch widgets\r\n\t\tvar tileCategories = (string[])_tileCategories.Clone();\r\n\t\tvar tileShapes = (string[])_tileShapes.Clone();\r\n\t\tvar tileSeeds = (long[])_tileSeeds.Clone();\r\n\t\tvar tileMinHeights = (float[])_tileMinHeights.Clone();\r\n\t\tvar tileMaxHeights = (float[])_tileMaxHeights.Clone();\r\n\t\tvar tilePlaneScales = (float[])_tilePlaneScales.Clone();\r\n\t\tvar tileSmoothing = (int[])_tileSmoothingPasses.Clone();\r\n\t\tvar tileNoiseLayers = (int[])_tileNoiseLayerStacks.Clone();\r\n\t\tvar tileWarping = (bool[])_tileDomainWarping.Clone();\r\n\t\tvar tileWarpingSizes = (float[])_tileDomainWarpingSizes.Clone();\r\n\t\tvar tileWarpingStrengths = (float[])_tileDomainWarpingStrengths.Clone();\r\n\t\tvar tileSplatLayerCounts = (int[])_tileSplatLayerCounts.Clone();\r\n\t\tvar tileSplatDispersions = (SplatDispersionMode[])_tileSplatDispersions.Clone();\r\n\t\tvar tileSplatBlends = (float[])_tileSplatBlendStrengths.Clone();\r\n\t\tint gridSize = TerrainGridSize;\r\n\t\tbool rivers = RiverCarvingBool;\r\n\t\tfloat riverFrequency = RiverCarvingFrequency;\r\n\t\tfloat riverWidth = RiverCarvingWidth;\r\n\t\tfloat riverDepth = RiverCarvingDepth;\r\n\t\tfloat riverTurbFreq = RiverCarvingTurbulenceFrequency;\r\n\t\tfloat riverTurbStrength = RiverCarvingTurbulenceStrength;\r\n\t\tfloat riverSpacing = RiverCarvingSpacing;\r\n\t\tbool staging = StagingArea;\r\n\t\tint stagingSize = StagingAreaSize;\r\n\t\tfloat stagingHeight = StagingAreaHeight;\r\n\t\tfloat stagingX = StagingAreaX;\r\n\t\tfloat stagingY = StagingAreaY;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// CPU-heavy work (noise, smoothing, rivers, splatmap) runs off the main thread\r\n\t\t\tfloat[,] heightmap = await Task.Run( () => BuildHeightmap(\r\n\t\t\t\tPreviewResolution, PreviewResolution,\r\n\t\t\t\ttileCategories, tileShapes, gridSize,\r\n\t\t\t\ttileSeeds, tileNoiseLayers, tileMinHeights, tileMaxHeights,\r\n\t\t\t\ttileWarping, tileWarpingSizes, tileWarpingStrengths,\r\n\t\t\t\ttileSmoothing, tilePlaneScales,\r\n\t\t\t\trivers, riverFrequency, riverWidth, riverDepth,\r\n\t\t\t\triverTurbFreq, riverTurbStrength, riverSpacing,\r\n\t\t\t\tstaging, stagingSize, stagingHeight, stagingX, stagingY ) );\r\n\r\n\t\t\tif ( token != _generationToken ) return;\r\n\t\t\tif ( heightmap is null ) return;\r\n\r\n\t\t\t_previewHeightmap = heightmap;\r\n\r\n\t\t\t// GPU/scene updates must happen back on the main thread\r\n\t\t\tUpdatePreviewTerrain( heightmap );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_isGenerating = false;\r\n\r\n\t\t\t// If more changes came in while we were busy, regenerate again\r\n\t\t\tif ( _previewDirty && token == _generationToken )\r\n\t\t\t{\r\n\t\t\t\t_previewDirty = false;\r\n\t\t\t\tRegeneratePreviewAsync();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tvoid OnPreFrame()\r\n\t{\r\n\t\tGizmoInstance.Input.IsHovered = IsActiveWindow && RenderCanvas.IsUnderMouse;\r\n\r\n\t\tvar isAltHeld = Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt );\r\n\t\tvar isLeftDown = Editor.Application.MouseButtons.HasFlag( MouseButtons.Left );\r\n\r\n\t\tvar isInteracting = false;\r\n\r\n\t\tif ( GizmoInstance.OrbitCamera( Camera, RenderCanvas, ref _orbitDistance ) )\r\n\t\t{\r\n\t\t\t// User is manually orbiting - don't auto-spin this frame\r\n\t\t\tisInteracting = true;\r\n\t\t\tGizmoInstance.Input.IsHovered = false;\r\n\t\t}\r\n\t\telse if ( isAltHeld )\r\n\t\t{\r\n\t\t\tisInteracting = true;\r\n\t\t}\r\n\t\telse if ( isLeftDown && GizmoInstance.Input.IsHovered )\r\n\t\t{\r\n\t\t\t// Click and drag in the preview to adjust pitch/yaw\r\n\t\t\tisInteracting = true;\r\n\r\n\t\t\tvar delta = Editor.Application.CursorDelta * 0.1f;\r\n\r\n\t\t\t_orbitPitch = Math.Clamp( _orbitPitch + delta.y, 5f, 85f );\r\n\t\t\t_orbitAngle += delta.x;\r\n\t\t\tif ( _orbitAngle >= 360f ) _orbitAngle -= 360f;\r\n\t\t\tif ( _orbitAngle < 0f ) _orbitAngle += 360f;\r\n\r\n\t\t\tPositionCameraForOrbit();\r\n\t\t}\r\n\r\n\t\tif ( !isInteracting && _autoSpin )\r\n\t\t{\r\n\t\t\t// Slowly rotate the camera around the terrain\r\n\t\t\t_orbitAngle += SpinSpeed * RealTime.Delta;\r\n\t\t\tif ( _orbitAngle >= 360f ) _orbitAngle -= 360f;\r\n\t\t\tPositionCameraForOrbit();\r\n\t\t}\r\n\r\n\t\t// Scroll wheel over the preview controls zoom\r\n\t\tif ( !isAltHeld && GizmoInstance.Input.IsHovered && MathF.Abs( Editor.Application.MouseWheelDelta.y ) > 0.001f )\r\n\t\t{\r\n\t\t\tvar wheelDelta = Editor.Application.MouseWheelDelta.y;\r\n\t\t\tZoomSlider.Value = Math.Clamp( ZoomSlider.Value + wheelDelta * 500f, ZoomSlider.Minimum, ZoomSlider.Maximum );\r\n\t\t\tUpdateOrbitFromZoom();\r\n\t\t}\r\n\r\n\t\tRenderCanvas.UpdateGizmoInputs( GizmoInstance.Input.IsHovered );\r\n\t}\r\n\r\n\tvoid PositionCameraForOrbit()\r\n\t{\r\n\t\tif ( Camera is null || !Camera.IsValid() ) return;\r\n\r\n\t\tfloat pitch = _orbitPitch;\r\n\t\tfloat yaw = _orbitAngle;\r\n\r\n\t\tvar offset = new Vector3(\r\n\t\t\tMathF.Sin( MathX.DegreeToRadian( yaw ) ) * MathF.Cos( MathX.DegreeToRadian( pitch ) ),\r\n\t\t\tMathF.Cos( MathX.DegreeToRadian( yaw ) ) * MathF.Cos( MathX.DegreeToRadian( pitch ) ),\r\n\t\t\tMathF.Sin( MathX.DegreeToRadian( pitch ) )\r\n\t\t) * _orbitDistance;\r\n\r\n\t\tCamera.WorldPosition = Vector3.Zero + offset;\r\n\t\tCamera.WorldRotation = Rotation.LookAt( (Vector3.Zero - offset).Normal, Vector3.Up );\r\n\t}\r\n\r\n\tvoid UpdateOrbitFromZoom()\r\n\t{\r\n\t\t// Higher slider value = closer to terrain (zoom in)\r\n\t\t_orbitDistance = ZoomSlider.Maximum + ZoomSlider.Minimum - ZoomSlider.Value;\r\n\t\tPositionCameraForOrbit();\r\n\t}\r\n\r\n\tvoid BuildSplatColors()\r\n\t{\r\n\t\t// The colors must be sampled at the ACTUAL threshold positions the splatmap uses, not at\r\n\t\t// evenly spaced positions. In Natural dispersion the layers sit at slope-weighted stops,\r\n\t\t// so even sampling would skip/misalign colors. The gradient's frame times ARE the\r\n\t\t// thresholds (ResampleSplatGradient positions them there), so read them directly.\r\n\t\tint layerCount = MaxTileSplatLayers();\r\n\r\n\t\tvar frames = SplatMapGradient.Colors;\r\n\t\tif ( frames != null && frames.Count() == layerCount && layerCount > 0 )\r\n\t\t{\r\n\t\t\t// Frames are ordered by time - use their exact positions and colors so the splatmap\r\n\t\t\t// and shader reflect exactly what the user set in the gradient widget.\r\n\t\t\tvar times = new float[layerCount];\r\n\t\t\tvar colors = new SKColor[layerCount];\r\n\t\t\tint i = 0;\r\n\t\t\tforeach ( var frame in frames )\r\n\t\t\t{\r\n\t\t\t\ttimes[i] = Math.Clamp( frame.Time, 0f, 1f );\r\n\t\t\t\tvar c = frame.Value.ToColor32();\r\n\t\t\t\tcolors[i] = new SKColor( c.r, c.g, c.b, c.a );\r\n\t\t\t\ti++;\r\n\t\t\t}\r\n\t\t\t_splatthresholds = times;\r\n\t\t\t_splatcolors = colors;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Fallback: no matching frame count yet (e.g. first build) - sample the gradient at the\r\n\t\t// same threshold positions the splatmap will use.\r\n\t\tfloat[] stops;\r\n\t\tvar source = _previewHeightmap ?? _heightmap;\r\n\t\tif ( SplatDispersion == SplatDispersionMode.Natural && source != null )\r\n\t\t\tstops = ComputeNaturalThresholds( source, layerCount );\r\n\t\telse\r\n\t\t\tstops = MakeEvenThresholds( layerCount );\r\n\r\n\t\tvar thresholdtime = new List<float>();\r\n\t\tvar mapgradients = new List<SKColor>();\r\n\t\tfor ( int i = 0; i < layerCount; i++ )\r\n\t\t{\r\n\t\t\tthresholdtime.Add( stops[i] );\r\n\r\n\t\t\tvar color = SplatMapGradient.Evaluate( Math.Clamp( stops[i], 0f, 1f ) ).ToColor32();\r\n\t\t\tmapgradients.Add( new SKColor( color.r, color.g, color.b, color.a ) );\r\n\t\t}\r\n\t\t_splatthresholds = thresholdtime.ToArray();\r\n\t\t_splatcolors = mapgradients.ToArray();\r\n\t}\r\n\r\n\tvoid RegeneratePreview()\r\n\t{\r\n\t\tif ( _previewTerrain is null || !_previewTerrain.IsValid() ) return;\r\n\t\tif ( string.IsNullOrEmpty( CategoryArray?.Selected ) || string.IsNullOrEmpty( ShapeArray?.Selected ) ) return;\r\n\r\n\t\tBuildSplatColors();\r\n\r\n\t\tvar heightmap = BuildHeightmap(\r\n\t\t\tPreviewResolution, PreviewResolution,\r\n\t\t\t(string[])_tileCategories.Clone(), (string[])_tileShapes.Clone(), TerrainGridSize,\r\n\t\t\t(long[])_tileSeeds.Clone(), (int[])_tileNoiseLayerStacks.Clone(),\r\n\t\t\t(float[])_tileMinHeights.Clone(), (float[])_tileMaxHeights.Clone(),\r\n\t\t\t(bool[])_tileDomainWarping.Clone(), (float[])_tileDomainWarpingSizes.Clone(), (float[])_tileDomainWarpingStrengths.Clone(),\r\n\t\t\t(int[])_tileSmoothingPasses.Clone(), (float[])_tilePlaneScales.Clone(),\r\n\t\t\tRiverCarvingBool, RiverCarvingFrequency, RiverCarvingWidth, RiverCarvingDepth,\r\n\t\t\tRiverCarvingTurbulenceFrequency, RiverCarvingTurbulenceStrength, RiverCarvingSpacing,\r\n\t\t\tStagingArea, StagingAreaSize, StagingAreaHeight, StagingAreaX, StagingAreaY );\r\n\t\tif ( heightmap is null ) return;\r\n\r\n\t\t_previewHeightmap = heightmap;\r\n\t\tUpdatePreviewTerrain( heightmap );\r\n\t}\r\n\r\n\tvoid UpdatePreviewTerrain( float[,] heightmap )\r\n\t{\r\n\t\tif ( _previewTerrain is null || !_previewTerrain.IsValid() ) return;\r\n\t\tif ( _previewStorage is null ) return;\r\n\r\n\t\tint res = heightmap.GetLength( 0 );\r\n\r\n\t\t// Resize the storage to match the incoming heightmap so Generate (full res) and the\r\n\t\t// live preview (PreviewResolution) both work without a buffer size mismatch.\r\n\t\tif ( _previewStorage.Resolution != res )\r\n\t\t\t_previewStorage.SetResolution( res );\r\n\r\n\t\t// Write the heightmap into the terrain storage (0..65535 maps across TerrainHeight)\r\n\t\tushort[] heightArray = new ushort[res * res];\r\n\t\tfor ( int y = 0; y < res; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < res; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat h = Math.Clamp( heightmap[x, y], 0f, 1f );\r\n\t\t\t\theightArray[y * res + x] = (ushort)Math.Clamp( (int)(h * 65535f), 0, 65535 );\r\n\t\t\t}\r\n\t\t}\r\n\t\t_previewStorage.HeightMap = heightArray;\r\n\r\n\t\t// Build the control map (which materials go where). When the material preview is\r\n\t\t// enabled and we have assigned bluedock materials, blend them by the splat map\r\n\t\t// exactly like the exported terrain would. Otherwise use the single default material.\r\n\t\tuint[] controlMap = new uint[res * res];\r\n\r\n\t\tbool useMaterials = _activePropsTab == \"Splat\" && PreviewSplatMaterials && _previewMaterials != null && _previewMaterials.Length > 0;\r\n\r\n\t\tif ( useMaterials )\r\n\t\t{\r\n\t\t\tfloat[,] splatmap = BuildTileGridSplatmap( heightmap, TerrainGridSize,\r\n\t\t\t\t(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );\r\n\r\n\t\t\tint matCount = _previewMaterials.Length;\r\n\t\t\tfor ( int y = 0; y < res; y++ )\r\n\t\t\t{\r\n\t\t\t\tfor ( int x = 0; x < res; x++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat layerPos = Math.Clamp( splatmap[x, y], 0f, matCount - 1f );\r\n\t\t\t\t\tint baseId = (int)MathF.Floor( layerPos );\r\n\t\t\t\t\tint overlayId = Math.Min( baseId + 1, matCount - 1 );\r\n\t\t\t\t\tbyte blend = (byte)Math.Clamp( (int)((layerPos - baseId) * 255f), 0, 255 );\r\n\r\n\t\t\t\t\tcontrolMap[y * res + x] = new CompactTerrainMaterial( (byte)baseId, (byte)overlayId, blend, false ).Packed;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t// Default: single material, no blending\r\n\t\t\tfor ( int i = 0; i < controlMap.Length; i++ )\r\n\t\t\t{\r\n\t\t\t\tcontrolMap[i] = new CompactTerrainMaterial( 0, 0, 0, false ).Packed;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_previewStorage.ControlMap = controlMap;\r\n\r\n\t\t// Assign the materials and push everything to the GPU\r\n\t\tif ( _previewMaterials != null )\r\n\t\t{\r\n\t\t\t_previewStorage.Materials.Clear();\r\n\t\t\t_previewStorage.Materials.AddRange( _previewMaterials );\r\n\t\t}\r\n\r\n\t\t// Only the terrain shows when we're on the Splat tab previewing the real materials.\r\n\t\t// The terrain must be enabled before touching its GPU state, otherwise SyncGPUTexture\r\n\t\t// throws - so sync only when it's going to be visible.\r\n\t\tif ( _previewTerrain != null ) _previewTerrain.Enabled = useMaterials;\r\n\r\n\t\tif ( useMaterials && _previewTerrain != null && _previewTerrain.IsValid() )\r\n\t\t{\r\n\t\t\t_previewTerrain.Create();\r\n\t\t\t_previewTerrain.SyncGPUTexture();\r\n\t\t\t_previewTerrain.UpdateMaterialsBuffer();\r\n\t\t}\r\n\r\n\t\t// Overlay logic: on the Splat tab with the material preview off, overlay the splatmap\r\n\t\t// colors so you can see the layer layout. On any other tab, overlay the original\r\n\t\t// height-based color we used to paint the mesh. When materials are previewing, no overlay.\r\n\t\tbool showSplatOverlay = _activePropsTab == \"Splat\" && !useMaterials;\r\n\t\tUpdateSplatOverlay( heightmap, !useMaterials, showSplatOverlay );\r\n\t}\r\n\r\n\tvoid UpdateSplatOverlay( float[,] heightmap, bool visible, bool splatColors )\r\n\t{\r\n\t\tif ( _splatOverlayRenderer is null || !_splatOverlayRenderer.IsValid() ) return;\r\n\r\n\t\tint res = heightmap.GetLength( 0 );\r\n\r\n\t\t// Store the target heightmap and the desired colors. The mesh itself is animated\r\n\t\t// toward this target in FrameUpdate so changes morph smoothly instead of snapping.\r\n\t\tif ( _overlayTargetHeights == null || _overlayTargetHeights.Length != res * res )\r\n\t\t{\r\n\t\t\t_overlayTargetHeights = new float[res * res];\r\n\t\t\t_overlayCurrentHeights = new float[res * res];\r\n\t\t}\r\n\r\n\t\tbool first = _splatOverlayRenderer.Model is null;\r\n\r\n\t\tfor ( int y = 0; y < res; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < res; x++ )\r\n\t\t\t{\r\n\t\t\t\t_overlayTargetHeights[y * res + x] = Math.Clamp( heightmap[x, y], 0f, 1f );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// If we've never built the mesh, snap to the current values so the first frame is correct.\r\n\t\tif ( first )\r\n\t\t{\r\n\t\t\tArray.Copy( _overlayTargetHeights, _overlayCurrentHeights, _overlayTargetHeights.Length );\r\n\t\t\t_overlayAnimating = false;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_overlayAnimating = true;\r\n\t\t}\r\n\r\n\t\t// Cache the splatmap (only changes when the heightmap regenerates). The vertex colors\r\n\t\t// are evaluated from the LIVE gradient each frame so they stay in sync with the widget.\r\n\t\t_overlayUseSplatColors = splatColors;\r\n\t\tif ( splatColors )\r\n\t\t\t_overlaySplatmap = BuildTileGridSplatmap( heightmap, TerrainGridSize,\r\n\t\t\t\t(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );\r\n\r\n\t\tif ( first )\r\n\t\t\tBuildOverlayMesh();\r\n\r\n\t\t// Toggle visibility\r\n\t\tif ( _splatOverlayGO != null ) _splatOverlayGO.Enabled = visible;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Called every frame - eases the overlay mesh from its current shape toward the target shape.\r\n\t/// </summary>\r\n\tvoid UpdateOverlayAnimation()\r\n\t{\r\n\t\tif ( _splatOverlayRenderer is null || !_splatOverlayRenderer.IsValid() ) return;\r\n\t\tif ( !_overlayAnimating || _overlayTargetHeights is null || _overlayCurrentHeights is null ) return;\r\n\r\n\t\tint count = _overlayTargetHeights.Length;\r\n\t\tif ( _overlayCurrentHeights.Length != count ) return;\r\n\r\n\t\t// Exponential approach - fast at first, settles smoothly\r\n\t\tfloat t = 1f - MathF.Exp( -PreviewMorphSpeed * RealTime.Delta );\r\n\r\n\t\tfloat maxDelta = 0f;\r\n\t\tfor ( int i = 0; i < count; i++ )\r\n\t\t{\r\n\t\t\tfloat delta = _overlayTargetHeights[i] - _overlayCurrentHeights[i];\r\n\t\t\t_overlayCurrentHeights[i] += delta * t;\r\n\t\t\tmaxDelta = MathF.Max( maxDelta, MathF.Abs( delta ) );\r\n\t\t}\r\n\r\n\t\t// Rebuild the mesh from the current (eased) heights. Colors are sampled from the\r\n\t\t// live gradient inside BuildOverlayMesh so they animate as smoothly as the widget.\r\n\t\tBuildOverlayMesh();\r\n\r\n\t\t// Stop once we're close enough\r\n\t\tif ( maxDelta < 0.001f )\r\n\t\t{\r\n\t\t\tArray.Copy( _overlayTargetHeights, _overlayCurrentHeights, count );\r\n\t\t\t_overlayAnimating = false;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid BuildOverlayMesh()\r\n\t{\r\n\t\tif ( _splatOverlayRenderer is null || !_splatOverlayRenderer.IsValid() ) return;\r\n\t\tif ( _overlayCurrentHeights is null ) return;\r\n\r\n\t\tint res = (int)MathF.Sqrt( _overlayCurrentHeights.Length );\r\n\t\tint vertexCount = res * res;\r\n\r\n\t\tconst float worldSize = PreviewTerrainSize;\r\n\t\tconst float worldHeight = PreviewTerrainHeight;\r\n\t\tfloat cellX = worldSize / res;\r\n\t\tfloat cellY = worldSize / res;\r\n\r\n\t\t// Color ease factor - the mesh morphs at half the gradient speed so the color\r\n\t\t// swipe across the terrain is smoother. First build snaps immediately.\r\n\t\t// Tile-selection grey/blue changes use a much faster rate so they snap quicker.\r\n\t\tfloat colorMorphSpeed = _overlayColorAnimating ? PreviewMorphSpeed * 4f : MeshMorphSpeed;\r\n\t\tfloat colorT = _overlayCurrentColors is null ? 1f : 1f - MathF.Exp( -colorMorphSpeed * RealTime.Delta );\r\n\r\n\t\tvar vertices = new Vertex[vertexCount];\r\n\t\tvar colors = _overlayCurrentColors ?? new Color32[vertexCount];\r\n\r\n\t\tbool useSplat = _overlayUseSplatColors && _overlaySplatmap != null;\r\n\r\n\t\t// When a specific tile is selected, only that tile keeps its colors - the rest go grey\r\n\t\tint grid = Math.Max( TerrainGridSize, 1 );\r\n\t\tint tileW = res / grid;\r\n\t\tint tileH = res / grid;\r\n\r\n\t\t// Track how far the colors moved so the refresh can stop once they settle\r\n\t\tfloat[] maxColorDelta = new float[1];\r\n\r\n\t\tParallel.For( 0, res, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < res; x++ )\r\n\t\t\t{\r\n\t\t\t\tint index = y * res + x;\r\n\t\t\t\tfloat h = _overlayCurrentHeights[index];\r\n\r\n\t\t\t\t// Local space - the overlay GO is parented to the centered terrain GO\r\n\t\t\t\tVector3 position = new Vector3( x * cellX, y * cellY, h * worldHeight );\r\n\r\n\t\t\t\tfloat hL = _overlayCurrentHeights[y * res + Math.Max( x - 1, 0 )];\r\n\t\t\t\tfloat hR = _overlayCurrentHeights[y * res + Math.Min( x + 1, res - 1 )];\r\n\t\t\t\tfloat hD = _overlayCurrentHeights[Math.Max( y - 1, 0 ) * res + x];\r\n\t\t\t\tfloat hU = _overlayCurrentHeights[Math.Min( y + 1, res - 1 ) * res + x];\r\n\r\n\t\t\t\tfloat dx = (hR - hL) * worldHeight / (2.0f * cellX);\r\n\t\t\t\tfloat dy = (hU - hD) * worldHeight / (2.0f * cellY);\r\n\r\n\t\t\t\tVector3 normal = new Vector3( -dx, -dy, 1.0f ).Normal;\r\n\r\n\t\t\t\t// Which grid tile does this vertex belong to?\r\n\t\t\t\tint tileX = Math.Min( x / Math.Max( tileW, 1 ), grid - 1 );\r\n\t\t\t\tint tileY = Math.Min( y / Math.Max( tileH, 1 ), grid - 1 );\r\n\t\t\t\tint tileIndex = tileY * grid + tileX;\r\n\t\t\t\tbool isSelectedTile = _selectedTileIndex < 0 || tileIndex == _selectedTileIndex;\r\n\r\n\t\t\t\t// Sample the target color from the LIVE gradient each frame, then ease the\r\n\t\t\t\t// mesh color toward it so the swipe lags behind the widget and looks smooth.\r\n\t\t\t\tColor targetColor;\r\n\t\t\t\tif ( !isSelectedTile )\r\n\t\t\t\t{\r\n\t\t\t\t\t// Wash out everything outside the selected tile\r\n\t\t\t\t\ttargetColor = Color.FromBytes( 235, 235, 235 );\r\n\t\t\t\t}\r\n\t\t\t\telse if ( useSplat )\r\n\t\t\t\t{\r\n\t\t\t\t\tint tileLayers = _tileSplatLayerCounts != null && tileIndex < _tileSplatLayerCounts.Length\r\n\t\t\t\t\t\t? Math.Max( _tileSplatLayerCounts[tileIndex], 2 ) : Math.Max( SplatLayerCount, 2 );\r\n\r\n\t\t\t\t\t// Sample the color from the same threshold-aligned color table the splatmap\r\n\t\t\t\t\t// image uses, so the 3D preview and the exported splatmap always agree.\r\n\t\t\t\t\tfloat layerPos = Math.Clamp( _overlaySplatmap[x, y], 0f, tileLayers - 1f );\r\n\t\t\t\t\tint layer0 = (int)MathF.Floor( layerPos );\r\n\t\t\t\t\tint layer1 = Math.Min( layer0 + 1, tileLayers - 1 );\r\n\t\t\t\t\tfloat t = layerPos - layer0;\r\n\r\n\t\t\t\t\tif ( _splatcolors != null && _splatcolors.Length > layer1 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar col0 = _splatcolors[layer0];\r\n\t\t\t\t\t\tvar col1 = _splatcolors[layer1];\r\n\t\t\t\t\t\ttargetColor = new Color(\r\n\t\t\t\t\t\t\tMathX.LerpTo( col0.Red / 255f, col1.Red / 255f, t ),\r\n\t\t\t\t\t\t\tMathX.LerpTo( col0.Green / 255f, col1.Green / 255f, t ),\r\n\t\t\t\t\t\t\tMathX.LerpTo( col0.Blue / 255f, col1.Blue / 255f, t ) );\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tColor c0 = SplatMapGradient.Evaluate( Math.Clamp( layer0 / (float)Math.Max( tileLayers - 1, 1 ), 0f, 1f ) );\r\n\t\t\t\t\t\tColor c1 = SplatMapGradient.Evaluate( Math.Clamp( layer1 / (float)Math.Max( tileLayers - 1, 1 ), 0f, 1f ) );\r\n\t\t\t\t\t\ttargetColor = Color.Lerp( c0, c1, t );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\t// The original height-based material color we painted on the mesh\r\n\t\t\t\t\ttargetColor = Color.Lerp( Color.FromBytes( 30, 90, 200 ), Color.FromBytes( 200, 185, 150 ), h );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar target32 = targetColor.ToColor32();\r\n\t\t\t\tvar current = colors[index];\r\n\r\n\t\t\t\tbyte r = (byte)MathX.LerpTo( current.r, target32.r, colorT );\r\n\t\t\t\tbyte g = (byte)MathX.LerpTo( current.g, target32.g, colorT );\r\n\t\t\t\tbyte b = (byte)MathX.LerpTo( current.b, target32.b, colorT );\r\n\t\t\t\tbyte a = (byte)MathX.LerpTo( current.a, target32.a, colorT );\r\n\t\t\t\tcolors[index] = new Color32( r, g, b, a );\r\n\r\n\t\t\t\tfloat delta = MathF.Abs( r - target32.r ) + MathF.Abs( g - target32.g ) + MathF.Abs( b - target32.b );\r\n\t\t\t\tmaxColorDelta[0] = MathF.Max( maxColorDelta[0], delta );\r\n\r\n\t\t\t\tvertices[index] = new Vertex( position, normal, normal, new Vector4( 0, 0, 0, 1 ) );\r\n\t\t\t\tvertices[index].Color = colors[index];\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t_overlayCurrentColors = colors;\r\n\r\n\t\t// Stop the color-only refresh once everything has eased to its target\r\n\t\tif ( _overlayColorAnimating && maxColorDelta[0] < 1f )\r\n\t\t{\r\n\t\t\t_overlayColorAnimating = false;\r\n\t\t}\r\n\r\n\t\t// Build indices once - the grid topology never changes\r\n\t\tvar indices = new List<int>();\r\n\t\tif ( _overlayMesh is null )\r\n\t\t{\r\n\t\t\tfor ( int y = 0; y < res - 1; y++ )\r\n\t\t\t{\r\n\t\t\t\tfor ( int x = 0; x < res - 1; x++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tint a = x + y * res;\r\n\t\t\t\t\tint b = (x + 1) + y * res;\r\n\t\t\t\t\tint c = (x + 1) + (y + 1) * res;\r\n\t\t\t\t\tint d = x + (y + 1) * res;\r\n\r\n\t\t\t\t\tindices.Add( a );\r\n\t\t\t\t\tindices.Add( b );\r\n\t\t\t\t\tindices.Add( c );\r\n\t\t\t\t\tindices.Add( a );\r\n\t\t\t\t\tindices.Add( c );\r\n\t\t\t\t\tindices.Add( d );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( _overlayMesh is null || !_overlayMesh.IsValid() )\r\n\t\t{\r\n\t\t\t_overlayMesh = new Mesh( _splatOverlayRenderer.MaterialOverride );\r\n\t\t\t_overlayMesh.CreateVertexBuffer( vertices.Length, vertices );\r\n\t\t\t_overlayMesh.CreateIndexBuffer( indices.Count, indices );\r\n\t\t\t_overlayMesh.Bounds = BBox.FromPositionAndSize( new Vector3( worldSize * 0.5f, worldSize * 0.5f, worldHeight * 0.5f ), new Vector3( worldSize, worldSize, worldHeight ) );\r\n\r\n\t\t\t_splatOverlayRenderer.Model = Model.Builder.AddMesh( _overlayMesh ).Create();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t// Update the existing vertex buffer in place - much faster than rebuilding the model\r\n\t\t\t_overlayMesh.SetVertexBufferData( vertices );\r\n\t\t}\r\n\t}\r\n\r\n\tfloat[,] BuildHeightmap( int width, int height,\r\n\t\tstring[] tileCategories,\r\n\t\tstring[] tileShapes,\r\n\t\tint gridSize,\r\n\t\tlong[] tileSeeds,\r\n\t\tint[] tileNoiseLayerStacks,\r\n\t\tfloat[] tileMinHeights,\r\n\t\tfloat[] tileMaxHeights,\r\n\t\tbool[] tileDomainWarping,\r\n\t\tfloat[] tileDomainWarpingSizes,\r\n\t\tfloat[] tileDomainWarpingStrengths,\r\n\t\tint[] tileSmoothingPasses,\r\n\t\tfloat[] tilePlaneScales,\r\n\t\tbool riverCarving,\r\n\t\tfloat riverFrequency,\r\n\t\tfloat riverWidth,\r\n\t\tfloat riverDepth,\r\n\t\tfloat riverTurbulenceFrequency,\r\n\t\tfloat riverTurbulenceStrength,\r\n\t\tfloat minRiverSpacing,\r\n\t\tbool stagingArea,\r\n\t\tint stagingAreaSize,\r\n\t\tfloat stagingAreaHeight,\r\n\t\tfloat stagingAreaX,\r\n\t\tfloat stagingAreaY )\r\n\t{\r\n\t\tint grid = Math.Max( gridSize, 1 );\r\n\t\tint tileW = width / grid;\r\n\t\tint tileH = height / grid;\r\n\r\n\t\t// Build each tile's heightmap, then stitch them together averaging overlapping edges.\r\n\t\tfloat[,] heightmap = BuildTileGrid( width, height, tileW, tileH, grid, tileCategories, tileShapes, tileSeeds, tileNoiseLayerStacks, tileMinHeights, tileMaxHeights, tileDomainWarping, tileDomainWarpingSizes, tileDomainWarpingStrengths, tileSmoothingPasses, tilePlaneScales );\r\n\r\n\t\tif ( ErosionSimulation )\r\n\t\t{\r\n\r\n\t\t}\r\n\r\n\t\tif ( heightmap == null )\r\n\t\t{\r\n\t\t\tLog.Error( \"Heightmap is not generated. Aborting.\" );\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\t// Rivers are carved across the whole combined map so they flow continuously through the tiles\r\n\t\tif ( riverCarving )\r\n\t\t{\r\n\t\t\theightmap = AddTurbulenceForRivers(\r\n\t\t\theightmap,\r\n\t\t\tseed: tileSeeds != null && tileSeeds.Length > 0 ? tileSeeds[0] : 0,\r\n\t\t\triverFrequency: riverFrequency,\r\n\t\t\triverWidth: riverWidth,\r\n\t\t\triverDepth: riverDepth,\r\n\t\t\tturbulenceFrequency: riverTurbulenceFrequency,\r\n\t\t\tturbulenceStrength: riverTurbulenceStrength,\r\n\t\t\tminRiverSpacing: minRiverSpacing,\r\n\t\t\tslopeSteepness:10f,\r\n\t\t\tterrainNoiseFrequency: 2.0f,\r\n\t\t\tterrainNoiseAmplitude: 0.5f\r\n\t\t);\r\n\t\t}\r\n\r\n\t\tif ( stagingArea )\r\n\t\t{\r\n\t\t\theightmap = AddStagingSquare(\r\n\t\t\theightmap,\r\n\t\t\tstagingAreaSize,\r\n\t\t\tstagingAreaHeight,\r\n\t\t\tstagingAreaX,\r\n\t\t\tstagingAreaY );\r\n\t\t}\r\n\r\n\t\treturn heightmap;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Generates each tile with its own category/shape/height/scale/seed, then stitches them into\r\n\t/// one heightmap. Adjacent tiles share a blend band so their edges are averaged and look continuous.\r\n\t/// </summary>\r\n\tfloat[,] BuildTileGrid( int width, int height, int tileW, int tileH, int grid, string[] categories, string[] shapes, long[] seeds, int[] noiseLayersArr, float[] minHeights, float[] maxHeights, bool[] warpingArr, float[] warpingSizesArr, float[] warpingStrengthsArr, int[] smoothingArr, float[] planeScales )\r\n\t{\r\n\t\tfloat[,] result = new float[width, height];\r\n\r\n\t\t// Single tile - just generate it directly at the requested size, matching the old behavior exactly.\r\n\t\tif ( grid <= 1 )\r\n\t\t{\r\n\t\t\tstring category = categories != null && categories.Length > 0 ? categories[0] : null;\r\n\t\t\tstring shape = shapes != null && shapes.Length > 0 ? shapes[0] : null;\r\n\t\t\tlong seed = seeds != null && seeds.Length > 0 ? seeds[0] : 0;\r\n\t\t\tint layerCount = noiseLayersArr != null && noiseLayersArr.Length > 0 ? noiseLayersArr[0] : 1;\r\n\t\t\tfloat minHeight = minHeights != null && minHeights.Length > 0 ? minHeights[0] : 0.2f;\r\n\t\t\tfloat maxHeight = maxHeights != null && maxHeights.Length > 0 ? maxHeights[0] : 0.5f;\r\n\t\t\tint smoothingPasses = smoothingArr != null && smoothingArr.Length > 0 ? smoothingArr[0] : 0;\r\n\t\t\tfloat planeScale = planeScales != null && planeScales.Length > 0 ? planeScales[0] : 0.5f;\r\n\t\t\tbool domainWarping = warpingArr != null && warpingArr.Length > 0 ? warpingArr[0] : true;\r\n\t\t\tfloat domainWarpingSize = warpingSizesArr != null && warpingSizesArr.Length > 0 ? warpingSizesArr[0] : 0.25f;\r\n\t\t\tfloat domainWarpingStrength = warpingStrengthsArr != null && warpingStrengthsArr.Length > 0 ? warpingStrengthsArr[0] : 0.15f;\r\n\r\n\t\t\tif ( string.IsNullOrEmpty( category ) ) category = \"Islands\";\r\n\t\t\tif ( string.IsNullOrEmpty( shape ) ) shape = \"Default\";\r\n\r\n\t\t\tvar fullclass = Type.GetType( $\"Sturnus.TerrainGenerationTool.{category}\" );\r\n\t\t\tif ( fullclass is null || fullclass.GetMethod( shape ) is null ) return null;\r\n\r\n\t\t\treturn GenerateStackedNoise(\r\n\t\t\t\twidth, height,\r\n\t\t\t\tseed,\r\n\t\t\t\tlayerCount,\r\n\t\t\t\t1.0f, 2.0f, 1.0f, 0.5f,\r\n\t\t\t\t( x, y ) => (float)CallMethod( $\"Sturnus.TerrainGenerationTool.{category}\", shape, new object[] {\r\n\t\t\t\tx, y,\r\n\t\t\t\twidth, height,\r\n\t\t\t\tseed,\r\n\t\t\t\tminHeight,\r\n\t\t\t\tdomainWarping,\r\n\t\t\t\tdomainWarpingSize,\r\n\t\t\t\tdomainWarpingStrength\r\n\t\t\t\t} ),\r\n\t\t\t\tmaxHeight,\r\n\t\t\t\tsmoothingPasses,\r\n\t\t\t\tplaneScale\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\tfloat[,] weight = new float[width, height];\r\n\r\n\t\t// Overlap width for edge blending - a fraction of the tile size so seams blend smoothly\r\n\t\tint blend = Math.Max( 2, Math.Min( tileW, tileH ) / 8 );\r\n\r\n\t\tfor ( int ty = 0; ty < grid; ty++ )\r\n\t\t{\r\n\t\t\tfor ( int tx = 0; tx < grid; tx++ )\r\n\t\t\t{\r\n\t\t\t\tint index = ty * grid + tx;\r\n\r\n\t\t\t\tstring category = categories != null && index < categories.Length && !string.IsNullOrEmpty( categories[index] ) ? categories[index] : \"Islands\";\r\n\t\t\t\tstring shape = shapes != null && index < shapes.Length && !string.IsNullOrEmpty( shapes[index] ) ? shapes[index] : \"Default\";\r\n\t\t\t\tlong tileSeed = seeds != null && index < seeds.Length ? seeds[index] : 0;\r\n\t\t\t\tint layerCount = noiseLayersArr != null && index < noiseLayersArr.Length ? noiseLayersArr[index] : 1;\r\n\t\t\t\tfloat minHeight = minHeights != null && index < minHeights.Length ? minHeights[index] : 0.2f;\r\n\t\t\t\tfloat maxHeight = maxHeights != null && index < maxHeights.Length ? maxHeights[index] : 0.5f;\r\n\t\t\t\tint smoothingPasses = smoothingArr != null && index < smoothingArr.Length ? smoothingArr[index] : 0;\r\n\t\t\t\tfloat planeScale = planeScales != null && index < planeScales.Length ? planeScales[index] : 0.5f;\r\n\t\t\t\tbool domainWarping = warpingArr != null && index < warpingArr.Length ? warpingArr[index] : true;\r\n\t\t\t\tfloat domainWarpingSize = warpingSizesArr != null && index < warpingSizesArr.Length ? warpingSizesArr[index] : 0.25f;\r\n\t\t\t\tfloat domainWarpingStrength = warpingStrengthsArr != null && index < warpingStrengthsArr.Length ? warpingStrengthsArr[index] : 0.15f;\r\n\r\n\t\t\t\tvar fullclass = Type.GetType( $\"Sturnus.TerrainGenerationTool.{category}\" );\r\n\t\t\t\tif ( fullclass is null || fullclass.GetMethod( shape ) is null )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tfloat[,] tile = GenerateStackedNoise(\r\n\t\t\t\t\ttileW + blend * 2,\r\n\t\t\t\t\ttileH + blend * 2,\r\n\t\t\t\t\ttileSeed,\r\n\t\t\t\t\tlayerCount,\r\n\t\t\t\t\t1.0f,\r\n\t\t\t\t\t2.0f,\r\n\t\t\t\t\t1.0f,\r\n\t\t\t\t\t0.5f,\r\n\t\t\t\t\t( x, y ) => (float)CallMethod( $\"Sturnus.TerrainGenerationTool.{category}\", shape, new object[] {\r\n\t\t\t\t\tx, y,\r\n\t\t\t\t\ttileW + blend * 2,\r\n\t\t\t\t\ttileH + blend * 2,\r\n\t\t\t\t\ttileSeed,\r\n\t\t\t\t\tminHeight,\r\n\t\t\t\t\tdomainWarping,\r\n\t\t\t\t\tdomainWarpingSize,\r\n\t\t\t\t\tdomainWarpingStrength\r\n\t\t\t\t\t} ),\r\n\t\t\t\t\tmaxHeight,\r\n\t\t\t\t\tsmoothingPasses,\r\n\t\t\t\t\tplaneScale\r\n\t\t\t\t);\r\n\r\n\t\t\t\tif ( tile is null ) continue;\r\n\r\n\t\t\t\t// Place the tile into the result with a weighted blend on the edges.\r\n\t\t\t\t// The tile is generated slightly larger than its slot (tileW + blend*2) so the\r\n\t\t\t\t// overlap regions between neighbors are averaged.\r\n\t\t\t\tint slotX = tx * tileW;\r\n\t\t\t\tint slotY = ty * tileH;\r\n\r\n\t\t\t\tfor ( int y = 0; y < tileH + blend * 2; y++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tint outY = slotY - blend + y;\r\n\t\t\t\t\tif ( outY < 0 || outY >= height ) continue;\r\n\r\n\t\t\t\t\tfor ( int x = 0; x < tileW + blend * 2; x++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tint outX = slotX - blend + x;\r\n\t\t\t\t\t\tif ( outX < 0 || outX >= width ) continue;\r\n\r\n\t\t\t\t\t\t// Edge weight: 1 in the core, fading to 0 across the blend band at each edge\r\n\t\t\t\t\t\tfloat wx = EdgeBlendWeight( x, tileW + blend * 2, blend );\r\n\t\t\t\t\t\tfloat wy = EdgeBlendWeight( y, tileH + blend * 2, blend );\r\n\t\t\t\t\t\tfloat w = wx * wy;\r\n\r\n\t\t\t\t\t\tresult[outX, outY] += tile[x, y] * w;\r\n\t\t\t\t\t\tweight[outX, outY] += w;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Normalize by the accumulated weights\r\n\t\tfor ( int y = 0; y < height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tif ( weight[x, y] > 0.0001f )\r\n\t\t\t\t\tresult[x, y] /= weight[x, y];\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Generates each grid cell as its own full-resolution heightmap using that cell's own\r\n\t/// category/shape/height/scale/seed/smoothing/noise/warp settings. No stitching - each cell\r\n\t/// is a complete map of width x height. Rivers and staging are applied per cell.\r\n\t/// </summary>\r\n\tList<float[,]> BuildPerCellHeightmaps( int width, int height,\r\n\t\tstring[] categories, string[] shapes, long[] seeds, int[] noiseLayersArr,\r\n\t\tfloat[] minHeights, float[] maxHeights, bool[] warpingArr, float[] warpingSizesArr,\r\n\t\tfloat[] warpingStrengthsArr, int[] smoothingArr, float[] planeScales,\r\n\t\tbool riverCarving, float riverFrequency, float riverWidth, float riverDepth,\r\n\t\tfloat riverTurbulenceFrequency, float riverTurbulenceStrength, float minRiverSpacing,\r\n\t\tbool stagingArea, int stagingAreaSize, float stagingAreaHeight, float stagingAreaX, float stagingAreaY )\r\n\t{\r\n\t\tvar cells = new List<float[,]>();\r\n\r\n\t\tint grid = Math.Max( TerrainGridSize, 1 );\r\n\t\tint count = grid * grid;\r\n\r\n\t\tfor ( int index = 0; index < count; index++ )\r\n\t\t{\r\n\t\t\tstring category = categories != null && index < categories.Length && !string.IsNullOrEmpty( categories[index] ) ? categories[index] : \"Islands\";\r\n\t\t\tstring shape = shapes != null && index < shapes.Length && !string.IsNullOrEmpty( shapes[index] ) ? shapes[index] : \"Default\";\r\n\t\t\tlong cellSeed = seeds != null && index < seeds.Length ? seeds[index] : 0;\r\n\t\t\tint layerCount = noiseLayersArr != null && index < noiseLayersArr.Length ? noiseLayersArr[index] : 1;\r\n\t\t\tfloat minHeight = minHeights != null && index < minHeights.Length ? minHeights[index] : 0.2f;\r\n\t\t\tfloat maxHeight = maxHeights != null && index < maxHeights.Length ? maxHeights[index] : 0.5f;\r\n\t\t\tbool domainWarping = warpingArr != null && index < warpingArr.Length ? warpingArr[index] : true;\r\n\t\t\tfloat domainWarpingSize = warpingSizesArr != null && index < warpingSizesArr.Length ? warpingSizesArr[index] : 0.25f;\r\n\t\t\tfloat domainWarpingStrength = warpingStrengthsArr != null && index < warpingStrengthsArr.Length ? warpingStrengthsArr[index] : 0.15f;\r\n\t\t\tint smoothingPasses = smoothingArr != null && index < smoothingArr.Length ? smoothingArr[index] : 0;\r\n\t\t\tfloat planeScale = planeScales != null && index < planeScales.Length ? planeScales[index] : 0.5f;\r\n\r\n\t\t\tvar fullclass = Type.GetType( $\"Sturnus.TerrainGenerationTool.{category}\" );\r\n\t\t\tif ( fullclass is null || fullclass.GetMethod( shape ) is null )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat[,] map = GenerateStackedNoise(\r\n\t\t\t\twidth, height,\r\n\t\t\t\tcellSeed,\r\n\t\t\t\tlayerCount,\r\n\t\t\t\t1.0f, 2.0f, 1.0f, 0.5f,\r\n\t\t\t\t( x, y ) => (float)CallMethod( $\"Sturnus.TerrainGenerationTool.{category}\", shape, new object[] {\r\n\t\t\t\t\tx, y,\r\n\t\t\t\t\twidth, height,\r\n\t\t\t\t\tcellSeed,\r\n\t\t\t\t\tminHeight,\r\n\t\t\t\t\tdomainWarping,\r\n\t\t\t\t\tdomainWarpingSize,\r\n\t\t\t\t\tdomainWarpingStrength\r\n\t\t\t\t} ),\r\n\t\t\t\tmaxHeight,\r\n\t\t\t\tsmoothingPasses,\r\n\t\t\t\tplaneScale\r\n\t\t\t);\r\n\r\n\t\t\tif ( map is null ) continue;\r\n\r\n\t\t\tif ( riverCarving )\r\n\t\t\t{\r\n\t\t\t\tmap = AddTurbulenceForRivers( map, cellSeed, riverFrequency, riverWidth, riverDepth,\r\n\t\t\t\t\triverTurbulenceFrequency, riverTurbulenceStrength, minRiverSpacing, 10f, 2.0f, 0.5f );\r\n\t\t\t}\r\n\r\n\t\t\tif ( stagingArea )\r\n\t\t\t{\r\n\t\t\t\tmap = AddStagingSquare( map, stagingAreaSize, stagingAreaHeight, stagingAreaX, stagingAreaY );\r\n\t\t\t}\r\n\r\n\t\t\tcells.Add( map );\r\n\t\t}\r\n\r\n\t\treturn cells;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Generates the splatmap cache for per-cell mode - one full-res splatmap per cell using that\r\n\t/// cell's own layer count / dispersion / blend strength.\r\n\t/// </summary>\r\n\tList<float[,]> BuildPerCellSplatmaps( List<float[,]> cellHeightmaps, int[] layerCounts, SplatDispersionMode[] dispersions, float[] blendStrengths )\r\n\t{\r\n\t\tvar splats = new List<float[,]>();\r\n\t\tif ( cellHeightmaps is null ) return splats;\r\n\r\n\t\tfor ( int i = 0; i < cellHeightmaps.Count; i++ )\r\n\t\t{\r\n\t\t\tint layers = layerCounts != null && i < layerCounts.Length ? Math.Max( layerCounts[i], 2 ) : 2;\r\n\t\t\tvar dispersion = dispersions != null && i < dispersions.Length ? dispersions[i] : SplatDispersionMode.Evenly;\r\n\t\t\tfloat blend = blendStrengths != null && i < blendStrengths.Length ? blendStrengths[i] : 0.35f;\r\n\r\n\t\t\tsplats.Add( GenerateSplatmap( cellHeightmaps[i], MakeEvenThresholds( layers ), TerrainMaxHeight, layers, dispersion, blend ) );\r\n\t\t}\r\n\r\n\t\treturn splats;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Generates a splatmap where each grid tile uses its own layer count, dispersion mode and\r\n\t/// blend strength. Each tile's splatmap is computed over its padded region (with the blend\r\n\t/// overlap) and stitched together using the same edge weights as the heightmap.\r\n\t/// </summary>\r\n\tfloat[,] BuildTileGridSplatmap( float[,] heightmap, int gridSize, int[] layerCounts, SplatDispersionMode[] dispersions, float[] blendStrengths )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\r\n\t\tint grid = Math.Max( gridSize, 1 );\r\n\t\tint tileW = width / grid;\r\n\t\tint tileH = height / grid;\r\n\r\n\t\tint maxLayers = 2;\r\n\t\tif ( layerCounts != null )\r\n\t\t{\r\n\t\t\tforeach ( var lc in layerCounts )\r\n\t\t\t\tmaxLayers = Math.Max( maxLayers, lc );\r\n\t\t}\r\n\r\n\t\t// Single tile - same as before, just uses the tile's settings.\r\n\t\tif ( grid <= 1 )\r\n\t\t{\r\n\t\t\tint layers = layerCounts != null && layerCounts.Length > 0 ? Math.Max( layerCounts[0], 2 ) : maxLayers;\r\n\t\t\tvar dispersion = dispersions != null && dispersions.Length > 0 ? dispersions[0] : SplatDispersionMode.Evenly;\r\n\t\t\tfloat blendStrength = blendStrengths != null && blendStrengths.Length > 0 ? blendStrengths[0] : 0.35f;\r\n\r\n\t\t\treturn GenerateSplatmap( heightmap, MakeEvenThresholds( layers ), TerrainMaxHeight, layers, dispersion, blendStrength );\r\n\t\t}\r\n\r\n\t\tfloat[,] result = new float[width, height];\r\n\t\tfloat[,] weight = new float[width, height];\r\n\r\n\t\tint blend = Math.Max( 2, Math.Min( tileW, tileH ) / 8 );\r\n\r\n\t\tfor ( int ty = 0; ty < grid; ty++ )\r\n\t\t{\r\n\t\t\tfor ( int tx = 0; tx < grid; tx++ )\r\n\t\t\t{\r\n\t\t\t\tint index = ty * grid + tx;\r\n\r\n\t\t\t\tint layers = layerCounts != null && index < layerCounts.Length ? Math.Max( layerCounts[index], 2 ) : maxLayers;\r\n\t\t\t\tvar dispersion = dispersions != null && index < dispersions.Length ? dispersions[index] : SplatDispersionMode.Evenly;\r\n\t\t\t\tfloat blendStrength = blendStrengths != null && index < blendStrengths.Length ? blendStrengths[index] : 0.35f;\r\n\r\n\t\t\t\t// Extract the tile's heightmap region (with blend padding) so its splatmap\r\n\t\t\t\t// normalizes against the tile's own height range.\r\n\t\t\t\tint tw = tileW + blend * 2;\r\n\t\t\t\tint th = tileH + blend * 2;\r\n\t\t\t\tfloat[,] tileHeight = new float[tw, th];\r\n\r\n\t\t\t\tint slotX = tx * tileW;\r\n\t\t\t\tint slotY = ty * tileH;\r\n\r\n\t\t\t\tfor ( int y = 0; y < th; y++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tint srcY = slotY - blend + y;\r\n\t\t\t\t\tif ( srcY < 0 || srcY >= height ) continue;\r\n\r\n\t\t\t\t\tfor ( int x = 0; x < tw; x++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tint srcX = slotX - blend + x;\r\n\t\t\t\t\t\tif ( srcX < 0 || srcX >= width ) continue;\r\n\r\n\t\t\t\t\t\ttileHeight[x, y] = heightmap[srcX, srcY];\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tfloat[,] tileSplat = GenerateSplatmap( tileHeight, MakeEvenThresholds( layers ), TerrainMaxHeight, layers, dispersion, blendStrength );\r\n\r\n\t\t\t\t// Stitch with edge weights - same as the heightmap tiles\r\n\t\t\t\tfor ( int y = 0; y < th; y++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tint outY = slotY - blend + y;\r\n\t\t\t\t\tif ( outY < 0 || outY >= height ) continue;\r\n\r\n\t\t\t\t\tfor ( int x = 0; x < tw; x++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tint outX = slotX - blend + x;\r\n\t\t\t\t\t\tif ( outX < 0 || outX >= width ) continue;\r\n\r\n\t\t\t\t\t\tfloat wx = EdgeBlendWeight( x, tw, blend );\r\n\t\t\t\t\t\tfloat wy = EdgeBlendWeight( y, th, blend );\r\n\t\t\t\t\t\tfloat w = wx * wy;\r\n\r\n\t\t\t\t\t\tresult[outX, outY] += tileSplat[x, y] * w;\r\n\t\t\t\t\t\tweight[outX, outY] += w;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Normalize by the accumulated weights\r\n\t\tfor ( int y = 0; y < height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tif ( weight[x, y] > 0.0001f )\r\n\t\t\t\t\tresult[x, y] /= weight[x, y];\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Builds evenly spaced color stop positions for the given layer count.\r\n\t/// </summary>\r\n\tfloat[] MakeEvenThresholds( int layers )\r\n\t{\r\n\t\tvar t = new float[layers];\r\n\t\tfor ( int i = 0; i < layers; i++ )\r\n\t\t{\r\n\t\t\tt[i] = layers <= 1 ? 0f : (float)i / (layers - 1);\r\n\t\t}\r\n\t\treturn t;\r\n\t}\r\n\r\n\tfloat EdgeBlendWeight( int coord, int size, int blend )\r\n\t{\r\n\t\tif ( blend <= 0 ) return 1f;\r\n\t\tif ( coord < blend ) return (float)coord / blend;\r\n\t\tif ( coord > size - blend ) return (float)(size - coord) / blend;\r\n\t\treturn 1f;\r\n\t}\r\n\r\n\tpublic void RebuildShapes()\r\n\t{\r\n\t\tShapeArray.DestroyChildren();\r\n\t\tTerrainShapeArray.Clear();\r\n\r\n\t\tif ( CategoryArray?.Selected is null )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tstring className = $\"Sturnus.TerrainGenerationTool.{TerrainCategoryEnum.GetName( TerrainCategoryEnum.GetValue( CategoryArray.Selected ) )}\"; // Fully qualified name\r\n\t\tstring[] methods = GetMethodsFromClass( className );\r\n\r\n\t\t// Print the methods\r\n\t\tforeach ( string method in methods )\r\n\t\t{\r\n\t\t\tTerrainShapeArray.Add( method );\r\n\t\t\t//Log.Info( method );\r\n\t\t}\r\n\r\n\t\tforeach ( var shape in TerrainShapeArray )\r\n\t\t{\r\n\t\t\tShapeArray.AddOption( shape );\r\n\t\t}\r\n\r\n\t\tShapeArray.SelectedIndex = 0;\r\n\t\tShapeArray.Selected = ShapeArray.Children.FirstOrDefault().Name;\r\n\t\tforeach(var test in ShapeArray.Children )\r\n\t\t{\r\n\t\t\t//Log.Info( test.Name );\r\n\t\t}\r\n\t\t\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Rebuilds the tile grid section: an \"All\" box plus one box per grid cell. Each box shows\r\n\t/// which terrain category that tile currently uses and is clickable to select which tile the\r\n\t/// Terrain Type page's category/shape selectors edit.\r\n\t/// </summary>\r\n\tvoid RebuildTileGridUI()\r\n\t{\r\n\t\tif ( _tilesContainer is null || !_tilesContainer.IsValid() ) return;\r\n\r\n\t\t_tilesContainer.DestroyChildren();\r\n\t\t_tileBoxes.Clear();\r\n\r\n\t\tint grid = Math.Max( TerrainGridSize, 1 );\r\n\t\tint count = grid * grid;\r\n\r\n\t\t// Preserve existing selections, extend/trim to the new size\r\n\t\tvar oldCategories = _tileCategories;\r\n\t\tvar oldShapes = _tileShapes;\r\n\t\tvar oldMinHeights = _tileMinHeights;\r\n\t\tvar oldMaxHeights = _tileMaxHeights;\r\n\t\tvar oldPlaneScales = _tilePlaneScales;\r\n\t\tvar oldSeeds = _tileSeeds;\r\n\t\tvar oldSmoothing = _tileSmoothingPasses;\r\n\t\tvar oldNoiseLayers = _tileNoiseLayerStacks;\r\n\t\tvar oldWarping = _tileDomainWarping;\r\n\t\tvar oldWarpingSizes = _tileDomainWarpingSizes;\r\n\t\tvar oldWarpingStrengths = _tileDomainWarpingStrengths;\r\n\t\tvar oldSplatLayerCounts = _tileSplatLayerCounts;\r\n\t\tvar oldSplatMapCounts = _tileSplatMapCounts;\r\n\t\tvar oldSplatDispersions = _tileSplatDispersions;\r\n\t\tvar oldSplatBlendStrengths = _tileSplatBlendStrengths;\r\n\r\n\t\t_tileCategories = new string[count];\r\n\t\t_tileShapes = new string[count];\r\n\t\t_tileMinHeights = new float[count];\r\n\t\t_tileMaxHeights = new float[count];\r\n\t\t_tilePlaneScales = new float[count];\r\n\t\t_tileSeeds = new long[count];\r\n\t\t_tileSmoothingPasses = new int[count];\r\n\t\t_tileNoiseLayerStacks = new int[count];\r\n\t\t_tileDomainWarping = new bool[count];\r\n\t\t_tileDomainWarpingSizes = new float[count];\r\n\t\t_tileDomainWarpingStrengths = new float[count];\r\n\t\t_tileSplatLayerCounts = new int[count];\r\n\t\t_tileSplatMapCounts = new int[count];\r\n\t\t_tileSplatDispersions = new SplatDispersionMode[count];\r\n\t\t_tileSplatBlendStrengths = new float[count];\r\n\r\n\t\tstring defaultCategory = TerrainCategoryArray.Count > 0 ? TerrainCategoryArray.First() : CategoryArray?.Selected;\r\n\t\tstring defaultShape = FirstShapeForCategory( defaultCategory );\r\n\t\tif ( string.IsNullOrEmpty( defaultShape ) )\r\n\t\t\tdefaultShape = ShapeArray?.Selected;\r\n\r\n\t\tfor ( int i = 0; i < count; i++ )\r\n\t\t{\r\n\t\t\t_tileCategories[i] = oldCategories != null && i < oldCategories.Length && !string.IsNullOrEmpty( oldCategories[i] )\r\n\t\t\t\t? oldCategories[i] : defaultCategory;\r\n\t\t\t_tileShapes[i] = oldShapes != null && i < oldShapes.Length && !string.IsNullOrEmpty( oldShapes[i] )\r\n\t\t\t\t? oldShapes[i] : defaultShape;\r\n\t\t\t_tileMinHeights[i] = oldMinHeights != null && i < oldMinHeights.Length ? oldMinHeights[i] : TerrainMinHeight;\r\n\t\t\t_tileMaxHeights[i] = oldMaxHeights != null && i < oldMaxHeights.Length ? oldMaxHeights[i] : TerrainMaxHeight;\r\n\t\t\t_tilePlaneScales[i] = oldPlaneScales != null && i < oldPlaneScales.Length ? oldPlaneScales[i] : TerrainPlaneScale;\r\n\t\t\t_tileSeeds[i] = oldSeeds != null && i < oldSeeds.Length ? oldSeeds[i] : TerrainSeed;\r\n\t\t\t_tileSmoothingPasses[i] = oldSmoothing != null && i < oldSmoothing.Length ? oldSmoothing[i] : SmoothingPasses;\r\n\t\t\t_tileNoiseLayerStacks[i] = oldNoiseLayers != null && i < oldNoiseLayers.Length ? oldNoiseLayers[i] : NoiseLayerStacks;\r\n\t\t\t_tileDomainWarping[i] = oldWarping != null && i < oldWarping.Length ? oldWarping[i] : DomainWarping;\r\n\t\t\t_tileDomainWarpingSizes[i] = oldWarpingSizes != null && i < oldWarpingSizes.Length ? oldWarpingSizes[i] : DomainWarpingSize;\r\n\t\t\t_tileDomainWarpingStrengths[i] = oldWarpingStrengths != null && i < oldWarpingStrengths.Length ? oldWarpingStrengths[i] : DomainWarpingStrength;\r\n\t\t\t_tileSplatLayerCounts[i] = oldSplatLayerCounts != null && i < oldSplatLayerCounts.Length ? oldSplatLayerCounts[i] : SplatLayerCount;\r\n\t\t\t_tileSplatMapCounts[i] = oldSplatMapCounts != null && i < oldSplatMapCounts.Length ? oldSplatMapCounts[i] : SplatMapCount;\r\n\t\t\t_tileSplatDispersions[i] = oldSplatDispersions != null && i < oldSplatDispersions.Length ? oldSplatDispersions[i] : SplatDispersion;\r\n\t\t\t_tileSplatBlendStrengths[i] = oldSplatBlendStrengths != null && i < oldSplatBlendStrengths.Length ? oldSplatBlendStrengths[i] : SplatBlendStrength;\r\n\t\t}\r\n\r\n\t\t_selectedTileIndex = Math.Clamp( _selectedTileIndex, 0, count - 1 );\r\n\r\n\t\t// \"All tiles\" box selects every tile at once\r\n\t\tvar allBox = new TileGridBox( null, -1, \"All\", \"Select every tile\" );\r\n\t\tallBox.IsSelected = _selectedTileIndex < 0;\r\n\t\tallBox.OnClicked = () => SelectTile( -1 );\r\n\t\t_tileBoxes.Add( allBox );\r\n\t\t_tilesContainer.Layout.Add( allBox );\r\n\r\n\t\t// One box per grid cell\r\n\t\tfor ( int ty = 0; ty < grid; ty++ )\r\n\t\t{\r\n\t\t\tvar row = _tilesContainer.Layout.AddRow();\r\n\t\t\trow.Spacing = 4;\r\n\r\n\t\t\tfor ( int tx = 0; tx < grid; tx++ )\r\n\t\t\t{\r\n\t\t\t\tint index = ty * grid + tx;\r\n\r\n\t\t\t\tvar box = new TileGridBox( null, index, $\"{_tileCategories[index]}:{_tileShapes[index]}\", $\"Tile {tx},{ty}\" );\r\n\t\t\t\tbox.IsSelected = index == _selectedTileIndex;\r\n\t\t\t\tbox.OnClicked = () => SelectTile( index );\r\n\t\t\t\t_tileBoxes.Add( box );\r\n\t\t\t\trow.Add( box, 1 );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Sync the category/shape selectors to whichever tile is selected\r\n\t\tSyncSelectorsToSelectedTile();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Marks the given tile as the one being edited. -1 selects all tiles.\r\n\t/// </summary>\r\n\tvoid SelectTile( int index )\r\n\t{\r\n\t\tif ( _selectedTileIndex == index ) return;\r\n\r\n\t\t_selectedTileIndex = index;\r\n\t\tUpdateTileBoxSelection();\r\n\t\tSyncSelectorsToSelectedTile();\r\n\r\n\t\t// Ease the overlay mesh colors so only the selected tile stays colored\r\n\t\tif ( _overlayMesh != null && _overlayMesh.IsValid() )\r\n\t\t{\r\n\t\t\t_overlayColorAnimating = true;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid UpdateTileBoxSelection()\r\n\t{\r\n\t\tforeach ( var box in _tileBoxes )\r\n\t\t{\r\n\t\t\tif ( box.Index == _selectedTileIndex )\r\n\t\t\t{\r\n\t\t\t\tbox.IsSelected = true;\r\n\t\t\t\tbox.Update();\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tbox.IsSelected = false;\r\n\t\t\t\tbox.Update();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tvoid UpdateTileBoxText()\r\n\t{\r\n\t\tforeach ( var box in _tileBoxes )\r\n\t\t{\r\n\t\t\tif ( box.Index < 0 ) continue;\r\n\t\t\tif ( box.Index < _tileCategories.Length && box.Index < _tileShapes.Length )\r\n\t\t\t\tbox.Text = $\"{_tileCategories[box.Index]}:{_tileShapes[box.Index]}\";\r\n\t\t\tbox.Update();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Pushes the selected tile's category/shape/height/scale/seed into the Terrain Type and\r\n\t/// Height/Scale page controls.\r\n\t/// </summary>\r\n\tvoid SyncSelectorsToSelectedTile()\r\n\t{\r\n\t\tif ( CategoryArray is null || ShapeArray is null ) return;\r\n\r\n\t\t_syncingTileSelectors = true;\r\n\r\n\t\tint refIndex = Math.Max( _selectedTileIndex, 0 );\r\n\t\tif ( refIndex >= _tileCategories.Length ) refIndex = 0;\r\n\r\n\t\tstring category = _tileCategories[refIndex];\r\n\t\tif ( CategoryArray.HasOption( category ) )\r\n\t\t{\r\n\t\t\tCategoryArray.Selected = category;\r\n\t\t}\r\n\t\telse if ( CategoryArray.Children.Count() > 0 )\r\n\t\t{\r\n\t\t\tCategoryArray.SelectedIndex = 0;\r\n\t\t}\r\n\r\n\t\tstring shape = _tileShapes[refIndex];\r\n\t\tif ( ShapeArray.HasOption( shape ) )\r\n\t\t{\r\n\t\t\tShapeArray.Selected = shape;\r\n\t\t}\r\n\t\telse if ( ShapeArray.Children.Count() > 0 )\r\n\t\t{\r\n\t\t\tShapeArray.SelectedIndex = 0;\r\n\t\t}\r\n\r\n\t\t// Push the selected tile's height/scale/seed into the global props so the\r\n\t\t// Height/Scale page sliders show the selected tile's values.\r\n\t\t_serialized.GetProperty( nameof( TerrainMinHeight ) )?.SetValue( _tileMinHeights[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( TerrainMaxHeight ) )?.SetValue( _tileMaxHeights[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( TerrainPlaneScale ) )?.SetValue( _tilePlaneScales[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( TerrainSeed ) )?.SetValue( _tileSeeds[refIndex] );\r\n\r\n\t\t// Same for the Smooth/Noise page controls\r\n\t\t_serialized.GetProperty( nameof( SmoothingPasses ) )?.SetValue( _tileSmoothingPasses[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( NoiseLayerStacks ) )?.SetValue( _tileNoiseLayerStacks[refIndex] );\r\n\r\n\t\t// Domain warping page controls\r\n\t\t_serialized.GetProperty( nameof( DomainWarping ) )?.SetValue( _tileDomainWarping[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( DomainWarpingSize ) )?.SetValue( _tileDomainWarpingSizes[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( DomainWarpingStrength ) )?.SetValue( _tileDomainWarpingStrengths[refIndex] );\r\n\r\n\t\t// Splat page controls\r\n\t\t_serialized.GetProperty( nameof( SplatLayerCount ) )?.SetValue( _tileSplatLayerCounts[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( SplatMapCount ) )?.SetValue( _tileSplatMapCounts[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( SplatDispersion ) )?.SetValue( _tileSplatDispersions[refIndex] );\r\n\t\t_serialized.GetProperty( nameof( SplatBlendStrength ) )?.SetValue( _tileSplatBlendStrengths[refIndex] );\r\n\r\n\t\t_syncingTileSelectors = false;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Called when the Terrain Type page's category changes. Writes the new category to the\r\n\t/// selected tile (or every tile if All is selected).\r\n\t/// </summary>\r\n\tvoid ApplySelectedCategory()\r\n\t{\r\n\t\tif ( _syncingTileSelectors ) return;\r\n\r\n\t\tstring category = CategoryArray.Selected;\r\n\t\tif ( string.IsNullOrEmpty( category ) ) return;\r\n\r\n\t\tif ( _selectedTileIndex < 0 )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i < _tileCategories.Length; i++ )\r\n\t\t\t{\r\n\t\t\t\t_tileCategories[i] = category;\r\n\t\t\t\t_tileShapes[i] = FirstShapeForCategory( category );\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( _selectedTileIndex < _tileCategories.Length )\r\n\t\t{\r\n\t\t\t_tileCategories[_selectedTileIndex] = category;\r\n\t\t\t_tileShapes[_selectedTileIndex] = FirstShapeForCategory( category );\r\n\t\t}\r\n\r\n\t\t// Keep the shape selector in sync with the new category's first shape\r\n\t\tif ( ShapeArray.HasOption( _tileShapes[Math.Max( _selectedTileIndex, 0 )] ) )\r\n\t\t\tShapeArray.Selected = _tileShapes[Math.Max( _selectedTileIndex, 0 )];\r\n\r\n\t\tUpdateTileBoxText();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Called when the Terrain Type page's shape changes. Writes the new shape to the selected\r\n\t/// tile (or every tile if All is selected).\r\n\t/// </summary>\r\n\tvoid ApplySelectedShape()\r\n\t{\r\n\t\tif ( _syncingTileSelectors ) return;\r\n\r\n\t\tstring shape = ShapeArray.Selected;\r\n\t\tif ( string.IsNullOrEmpty( shape ) ) return;\r\n\r\n\t\tif ( _selectedTileIndex < 0 )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i < _tileShapes.Length; i++ )\r\n\t\t\t\t_tileShapes[i] = shape;\r\n\t\t}\r\n\t\telse if ( _selectedTileIndex < _tileShapes.Length )\r\n\t\t{\r\n\t\t\t_tileShapes[_selectedTileIndex] = shape;\r\n\t\t}\r\n\r\n\t\tUpdateTileBoxText();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Writes the given global height/scale/seed values into the selected tile (or every tile\r\n\t/// if All is selected). Nullable params mean \"leave unchanged\".\r\n\t/// </summary>\r\n\tvoid WriteSelectedValues( float? minHeight = null, float? maxHeight = null, float? planeScale = null, long? seed = null, int? smoothing = null, int? noiseLayers = null,\r\n\t\tbool? warp = null, float? warpSize = null, float? warpStrength = null,\r\n\t\tint? splatLayers = null, int? splatMaps = null, SplatDispersionMode? splatDispersion = null, float? splatBlend = null )\r\n\t{\r\n\t\tif ( _selectedTileIndex < 0 )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i < _tileMinHeights.Length; i++ )\r\n\t\t\t{\r\n\t\t\t\tif ( minHeight.HasValue ) _tileMinHeights[i] = minHeight.Value;\r\n\t\t\t\tif ( maxHeight.HasValue ) _tileMaxHeights[i] = maxHeight.Value;\r\n\t\t\t\tif ( planeScale.HasValue ) _tilePlaneScales[i] = planeScale.Value;\r\n\t\t\t\tif ( seed.HasValue ) _tileSeeds[i] = seed.Value;\r\n\t\t\t\tif ( smoothing.HasValue ) _tileSmoothingPasses[i] = smoothing.Value;\r\n\t\t\t\tif ( noiseLayers.HasValue ) _tileNoiseLayerStacks[i] = noiseLayers.Value;\r\n\t\t\t\tif ( warp.HasValue ) _tileDomainWarping[i] = warp.Value;\r\n\t\t\t\tif ( warpSize.HasValue ) _tileDomainWarpingSizes[i] = warpSize.Value;\r\n\t\t\t\tif ( warpStrength.HasValue ) _tileDomainWarpingStrengths[i] = warpStrength.Value;\r\n\t\t\t\tif ( splatLayers.HasValue ) _tileSplatLayerCounts[i] = splatLayers.Value;\r\n\t\t\t\tif ( splatMaps.HasValue ) _tileSplatMapCounts[i] = splatMaps.Value;\r\n\t\t\t\tif ( splatDispersion.HasValue ) _tileSplatDispersions[i] = splatDispersion.Value;\r\n\t\t\t\tif ( splatBlend.HasValue ) _tileSplatBlendStrengths[i] = splatBlend.Value;\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( _selectedTileIndex < _tileMinHeights.Length )\r\n\t\t{\r\n\t\t\tif ( minHeight.HasValue ) _tileMinHeights[_selectedTileIndex] = minHeight.Value;\r\n\t\t\tif ( maxHeight.HasValue ) _tileMaxHeights[_selectedTileIndex] = maxHeight.Value;\r\n\t\t\tif ( planeScale.HasValue ) _tilePlaneScales[_selectedTileIndex] = planeScale.Value;\r\n\t\t\tif ( seed.HasValue ) _tileSeeds[_selectedTileIndex] = seed.Value;\r\n\t\t\tif ( smoothing.HasValue ) _tileSmoothingPasses[_selectedTileIndex] = smoothing.Value;\r\n\t\t\tif ( noiseLayers.HasValue ) _tileNoiseLayerStacks[_selectedTileIndex] = noiseLayers.Value;\r\n\t\t\tif ( warp.HasValue ) _tileDomainWarping[_selectedTileIndex] = warp.Value;\r\n\t\t\tif ( warpSize.HasValue ) _tileDomainWarpingSizes[_selectedTileIndex] = warpSize.Value;\r\n\t\t\tif ( warpStrength.HasValue ) _tileDomainWarpingStrengths[_selectedTileIndex] = warpStrength.Value;\r\n\t\t\tif ( splatLayers.HasValue ) _tileSplatLayerCounts[_selectedTileIndex] = splatLayers.Value;\r\n\t\t\tif ( splatMaps.HasValue ) _tileSplatMapCounts[_selectedTileIndex] = splatMaps.Value;\r\n\t\t\tif ( splatDispersion.HasValue ) _tileSplatDispersions[_selectedTileIndex] = splatDispersion.Value;\r\n\t\t\tif ( splatBlend.HasValue ) _tileSplatBlendStrengths[_selectedTileIndex] = splatBlend.Value;\r\n\t\t}\r\n\t}\r\n\r\n\tstring FirstShapeForCategory( string category )\r\n\t{\r\n\t\tvar options = ShapeOptionsForCategory( category );\r\n\t\treturn options.Length > 0 ? options[0] : null;\r\n\t}\r\n\r\n\tstring[] ShapeOptionsForCategory( string category )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( category ) ) return Array.Empty<string>();\r\n\r\n\t\tstring className = $\"Sturnus.TerrainGenerationTool.{category}\";\r\n\t\ttry\r\n\t\t{\r\n\t\t\treturn GetMethodsFromClass( className );\r\n\t\t}\r\n\t\tcatch\r\n\t\t{\r\n\t\t\treturn Array.Empty<string>();\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void UpdateTerrain()\r\n\t{\r\n\t\tif ( _heightmap is null ) return;\r\n\r\n\t\tvar ActiveScene = Editor.SceneEditorSession.Active.Scene;\r\n\t\tvar FirstTerrain = ActiveScene.GetAllComponents<Terrain>().FirstOrDefault();\r\n\t\tif ( !FirstTerrain.IsValid() ) return;\r\n\r\n\t\tint res = _heightmap.GetLength( 0 );\r\n\r\n\t\t// Resize the scene terrain's storage to match the generated heightmap\r\n\t\tif ( FirstTerrain.Storage is null )\r\n\t\t\tFirstTerrain.Storage = new TerrainStorage { EmbeddedResource = new Sandbox.Resources.EmbeddedResource { ResourceCompiler = \"embed\" } };\r\n\r\n\t\tFirstTerrain.Storage.SetResolution( res );\r\n\r\n\t\t// Write the heightmap with the same indexing the preview uses (heightArray[y * res + x] =\r\n\t\t// heightmap[x, y]). ConvertFloatArrayToUShortArray stores a transpose, which would mirror\r\n\t\t// the terrain against the splatmap and misalign the materials on slopes.\r\n\t\tushort[] heightArray = new ushort[res * res];\r\n\t\tfor ( int y = 0; y < res; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < res; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat h = Math.Clamp( _heightmap[x, y], 0f, 1f );\r\n\t\t\t\theightArray[y * res + x] = (ushort)Math.Clamp( (int)(h * 65535f), 0, 65535 );\r\n\t\t\t}\r\n\t\t}\r\n\t\tFirstTerrain.Storage.HeightMap = heightArray;\r\n\r\n\t\t// Apply the splatmap as a control map so the material blending matches the generated\r\n\t\t// splatmap. SetResolution wipes the control map, so we always rewrite it here. The splatmap\r\n\t\t// is recomputed from the current settings so dispersion/layer changes made after Generate\r\n\t\t// are honoured.\r\n\t\t_splatmap = BuildTileGridSplatmap( _heightmap, TerrainGridSize,\r\n\t\t\t(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );\r\n\r\n\t\tif ( _splatmap != null )\r\n\t\t{\r\n\t\t\t// Resolve which materials to use: the assigned preview materials, else the terrain's\r\n\t\t\t// existing materials, else fall back to loading local tmats.\r\n\t\t\tvar materials = _previewMaterials;\r\n\t\t\tif ( materials == null || materials.Length == 0 )\r\n\t\t\t\tmaterials = FirstTerrain.Storage.Materials?.ToArray();\r\n\r\n\t\t\tif ( materials == null || materials.Length == 0 )\r\n\t\t\t\tmaterials = LoadTerrainMaterialsSync();\r\n\r\n\t\t\tif ( materials != null && materials.Length > 0 )\r\n\t\t\t{\r\n\t\t\t\tuint[] controlMap = new uint[res * res];\r\n\t\t\t\tint matCount = materials.Length;\r\n\t\t\t\tfor ( int y = 0; y < res; y++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfor ( int x = 0; x < res; x++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat layerPos = Math.Clamp( _splatmap[x, y], 0f, matCount - 1f );\r\n\t\t\t\t\t\tint baseId = (int)MathF.Floor( layerPos );\r\n\t\t\t\t\t\tint overlayId = Math.Min( baseId + 1, matCount - 1 );\r\n\t\t\t\t\t\tbyte blend = (byte)Math.Clamp( (int)((layerPos - baseId) * 255f), 0, 255 );\r\n\t\t\t\t\t\tcontrolMap[y * res + x] = new CompactTerrainMaterial( (byte)baseId, (byte)overlayId, blend, false ).Packed;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tFirstTerrain.Storage.ControlMap = controlMap;\r\n\t\t\t\tFirstTerrain.Storage.Materials.Clear();\r\n\t\t\t\tFirstTerrain.Storage.Materials.AddRange( materials );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tFirstTerrain.Create();\r\n\t\tFirstTerrain.SyncGPUTexture();\r\n\t\tFirstTerrain.UpdateMaterialsBuffer();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Synchronously loads usable local .tmat terrain materials so Apply can set the splat\r\n\t/// control map even when the user never clicked \"Randomize Materials\".\r\n\t/// </summary>\r\n\tTerrainMaterial[] LoadTerrainMaterialsSync()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( _localTmatAssets is null || _localTmatAssets.Count == 0 )\r\n\t\t\t{\r\n\t\t\t\tvar allLocal = Editor.AssetSystem.All\r\n\t\t\t\t\t.Where( a => a is not null && !a.IsDeleted && !a.IsCloud )\r\n\t\t\t\t\t.Where( a => (a.RelativePath?.EndsWith( \".tmat\" ) ?? false) )\r\n\t\t\t\t\t.ToList();\r\n\t\t\t\tvar with1k = allLocal.Where( a => a.RelativePath.Contains( \"_1k\" ) ).ToList();\r\n\t\t\t\t_localTmatAssets = with1k.Count > 0 ? with1k : allLocal;\r\n\t\t\t}\r\n\r\n\t\t\tvar pool = new List<Editor.Asset>( _localTmatAssets );\r\n\t\t\tvar materials = new List<TerrainMaterial>();\r\n\r\n\t\t\tint layerCount = MaxTileSplatLayers();\r\n\t\t\twhile ( materials.Count < layerCount && pool.Count > 0 )\r\n\t\t\t{\r\n\t\t\t\tint idx = Random.Shared.Next( pool.Count );\r\n\t\t\t\tvar asset = pool[idx];\r\n\t\t\t\tpool.RemoveAt( idx );\r\n\r\n\t\t\t\tif ( !asset.TryLoadResource<TerrainMaterial>( out var found ) || found is null )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tif ( !IsMaterialUsable( found, asset.Path ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tmaterials.Add( found );\r\n\t\t\t}\r\n\r\n\t\t\treturn materials.Count > 0 ? materials.ToArray() : null;\r\n\t\t}\r\n\t\tcatch ( System.Exception e )\r\n\t\t{\r\n\t\t\tLog.Error( $\"Failed to load terrain materials for apply: {e.Message}\" );\r\n\t\t\treturn null;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Applies per-cell mode: spawns one Terrain per grid cell, each at full resolution and\r\n\t/// positioned so they tile together in the scene.\r\n\t/// </summary>\r\n\tprivate void UpdatePerCellTerrains()\r\n\t{\r\n\t\tif ( _cellHeightmaps == null || _cellHeightmaps.Count == 0 ) return;\r\n\r\n\t\tvar ActiveScene = Editor.SceneEditorSession.Active.Scene;\r\n\r\n\t\t// Match the size/height of an existing terrain in the scene so the cells line up,\r\n\t\t// falling back to the preview constants if the scene has no terrain yet.\r\n\t\tvar existing = ActiveScene.GetAllComponents<Terrain>().FirstOrDefault();\r\n\t\tfloat cellSize = existing.IsValid() ? existing.TerrainSize : PreviewTerrainSize;\r\n\t\tfloat terrainHeight = existing.IsValid() ? existing.TerrainHeight : PreviewTerrainHeight;\r\n\r\n\t\tint grid = Math.Max( TerrainGridSize, 1 );\r\n\r\n\t\t// Terrain size is a property on the component; size each cell so the whole grid spans cellSize*grid.\r\n\t\tfloat sizePerCell = cellSize;\r\n\r\n\t\tint cellRes = _cellHeightmaps[0].GetLength( 0 );\r\n\r\n\t\t// Recompute the per-cell splatmaps from the current settings so dispersion/layer changes\r\n\t\t// made after Generate are honoured.\r\n\t\t_cellSplatmaps = BuildPerCellSplatmaps( _cellHeightmaps,\r\n\t\t\t(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );\r\n\r\n\t\tusing ( ActiveScene.Push() )\r\n\t\t{\r\n\t\t\tfor ( int ty = 0; ty < grid; ty++ )\r\n\t\t\t{\r\n\t\t\t\tfor ( int tx = 0; tx < grid; tx++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tint index = ty * grid + tx;\r\n\t\t\t\t\tif ( index >= _cellHeightmaps.Count ) continue;\r\n\r\n\t\t\t\t\tvar go = new GameObject( true, $\"terrain cell {tx},{ty}\" );\r\n\t\t\t\t\tvar terrain = go.AddComponent<Terrain>( false );\r\n\r\n\t\t\t\t\tvar storage = new TerrainStorage();\r\n\t\t\t\t\tstorage.EmbeddedResource = new Sandbox.Resources.EmbeddedResource { ResourceCompiler = \"embed\" };\r\n\t\t\t\t\tstorage.SetResolution( cellRes );\r\n\t\t\t\t\tstorage.TerrainSize = sizePerCell;\r\n\t\t\t\t\tstorage.TerrainHeight = terrainHeight;\r\n\r\n\t\t\t\t\t// Match the preview's indexing (heightArray[y * res + x] = map[x, y]) so the\r\n\t\t\t\t\t// heightmap and splatmap line up on slopes.\r\n\t\t\t\t\tushort[] cellHeight = new ushort[cellRes * cellRes];\r\n\t\t\t\t\tvar cellMap = _cellHeightmaps[index];\r\n\t\t\t\t\tfor ( int y = 0; y < cellRes; y++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfor ( int x = 0; x < cellRes; x++ )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tfloat h = Math.Clamp( cellMap[x, y], 0f, 1f );\r\n\t\t\t\t\t\t\tcellHeight[y * cellRes + x] = (ushort)Math.Clamp( (int)(h * 65535f), 0, 65535 );\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\tstorage.HeightMap = cellHeight;\r\n\r\n\t\t\t\t\t// Add the splat control map so the material blending matches the generated splatmap.\r\n\t\t\t\t\t// These are fresh cells, so resolve materials the same way as the combined apply.\r\n\t\t\t\t\tif ( _cellSplatmaps != null && index < _cellSplatmaps.Count )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar materials = _previewMaterials;\r\n\t\t\t\t\t\tif ( materials == null || materials.Length == 0 )\r\n\t\t\t\t\t\t\tmaterials = LoadTerrainMaterialsSync();\r\n\r\n\t\t\t\t\t\tif ( materials != null && materials.Length > 0 )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tuint[] controlMap = new uint[cellRes * cellRes];\r\n\t\t\t\t\t\t\tint matCount = materials.Length;\r\n\t\t\t\t\t\t\tvar splatmap = _cellSplatmaps[index];\r\n\t\t\t\t\t\t\tfor ( int y = 0; y < cellRes; y++ )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tfor ( int x = 0; x < cellRes; x++ )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\tfloat layerPos = Math.Clamp( splatmap[x, y], 0f, matCount - 1f );\r\n\t\t\t\t\t\t\t\t\tint baseId = (int)MathF.Floor( layerPos );\r\n\t\t\t\t\t\t\t\t\tint overlayId = Math.Min( baseId + 1, matCount - 1 );\r\n\t\t\t\t\t\t\t\t\tbyte blend = (byte)Math.Clamp( (int)((layerPos - baseId) * 255f), 0, 255 );\r\n\t\t\t\t\t\t\t\t\tcontrolMap[y * cellRes + x] = new CompactTerrainMaterial( (byte)baseId, (byte)overlayId, blend, false ).Packed;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\tstorage.ControlMap = controlMap;\r\n\t\t\t\t\t\t\tstorage.Materials.Clear();\r\n\t\t\t\t\t\t\tstorage.Materials.AddRange( materials );\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tterrain.Storage = storage;\r\n\t\t\t\t\tterrain.TerrainSize = sizePerCell;\r\n\t\t\t\t\tterrain.TerrainHeight = terrainHeight;\r\n\r\n\t\t\t\t\t// Each cell is sizePerCell wide - tile them so the whole grid spans cellSize*grid\r\n\t\t\t\t\tgo.WorldPosition = new Vector3( tx * sizePerCell, ty * sizePerCell, 0f );\r\n\r\n\t\t\t\t\tterrain.Create();\r\n\t\t\t\t\tterrain.SyncGPUTexture();\r\n\t\t\t\t\tterrain.UpdateMaterialsBuffer();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate float[,] AddStagingSquare( float[,] heightmap, int squareSize, float squareHeight, float centerX, float centerY )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\r\n\t\t// Calculate the center and bounds of the square\r\n\t\tint centerXPixel = (int)(centerX * width);\r\n\t\tint centerYPixel = (int)(centerY * height);\r\n\t\tint halfSize = squareSize / 2;\r\n\r\n\t\tint startX = Math.Max( centerXPixel - halfSize, 0 );\r\n\t\tint startY = Math.Max( centerYPixel - halfSize, 0 );\r\n\t\tint endX = Math.Min( centerXPixel + halfSize, width - 1 );\r\n\t\tint endY = Math.Min( centerYPixel + halfSize, height - 1 );\r\n\r\n\t\t// Set the height values inside the square to be completely flat\r\n\t\tfor ( int y = startY; y <= endY; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = startX; x <= endX; x++ )\r\n\t\t\t{\r\n\t\t\t\theightmap[x, y] = squareHeight;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Add the slope around the square\r\n\t\tfor ( int y = 0; y < height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\t// Skip the flat square area\r\n\t\t\t\tif ( x >= startX && x <= endX && y >= startY && y <= endY )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t// Calculate the distance to the nearest edge of the square\r\n\t\t\t\tint dx = Math.Max( Math.Abs( x - centerXPixel ) - halfSize, 0 );\r\n\t\t\t\tint dy = Math.Max( Math.Abs( y - centerYPixel ) - halfSize, 0 );\r\n\t\t\t\tfloat distanceToSquare = MathF.Sqrt( dx * dx + dy * dy );\r\n\r\n\t\t\t\t// Calculate the target height for the slope\r\n\t\t\t\tfloat slopeHeight = squareHeight - (distanceToSquare * 0.0038f); // 0.0038f is perfect for players\r\n\r\n\t\t\t\t// Ensure the slope transitions smoothly into the existing terrain\r\n\t\t\t\theightmap[x, y] = Math.Max( heightmap[x, y], slopeHeight );\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn heightmap;\r\n\t}\r\n\r\n\tprivate void GeneratePreviewFile( string path, out SKBitmap image, out SKBitmap splat )\r\n\t{\r\n\t\t//Create TerrainGenerationTool folder if it doesn't exist.\r\n\t\tDirectory.CreateDirectory( path );\r\n\t\tstring previewfile = Path.Combine( path, $\"TerrainGenerationUtility_preview.png\" );\r\n\t\tstring splatfile = Path.Combine( path, $\"TerrainGenerationUtility_splat_preview.png\" );\r\n\r\n\t\timage = HeightmapToBitMap( _heightmap );\r\n\t\tSaveImage( image, previewfile );\r\n\t\tsplat = SplatmapToBitMap( _splatmap, _splatcolors );\r\n\t\tSaveSplatmapAsPng( splat, splatfile );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Builds a fresh GPU texture from a bitmap so the preview widgets always get new data\r\n\t/// (the resource cache would otherwise return the same stale texture for the same path).\r\n\t/// </summary>\r\n\tTexture TextureFromBitmap( SKBitmap bitmap )\r\n\t{\r\n\t\tif ( bitmap is null ) return Texture.Invalid;\r\n\r\n\t\tint width = bitmap.Width;\r\n\t\tint height = bitmap.Height;\r\n\t\tbyte[] rgba = new byte[width * height * 4];\r\n\r\n\t\tfor ( int y = 0; y < height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tvar c = bitmap.GetPixel( x, y );\r\n\t\t\t\tint i = (y * width + x) * 4;\r\n\t\t\t\trgba[i + 0] = c.Red;\r\n\t\t\t\trgba[i + 1] = c.Green;\r\n\t\t\t\trgba[i + 2] = c.Blue;\r\n\t\t\t\trgba[i + 3] = c.Alpha;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn Texture.Create( width, height )\r\n\t\t\t.WithName( $\"TerrainGenerationPreview_{Environment.TickCount}\" )\r\n\t\t\t.WithData( rgba )\r\n\t\t\t.Finish();\r\n\t}\r\n\r\n\tprivate void GenerateImageFiles( string output_path )\r\n\t{\r\n\t\tstring UsingDomainWarping = \"\";\r\n\t\tstring UsingErosionEmulation = \"\";\r\n\t\tstring UsingWaterCarving = \"\";\r\n\r\n\t\tif ( DomainWarping )\r\n\t\t{\r\n\t\t\tUsingDomainWarping = \"_warp\";\r\n\t\t}\r\n\r\n\t\tif ( ErosionSimulation )\r\n\t\t{\r\n\t\t\tUsingErosionEmulation = \"_erosion\";\r\n\t\t}\r\n\r\n\t\tif ( RiverCarvingBool )\r\n\t\t{\r\n\t\t\tUsingWaterCarving = \"_watercarving\";\r\n\t\t}\r\n\r\n\t\t//Create TerrainGenerationTool folder if it doesn't exist.\r\n\t\tDirectory.CreateDirectory( output_path );\r\n\r\n\t\tif ( GridStorage == GridStorageMode.PerCell && _cellHeightmaps != null && _cellHeightmaps.Count > 0 )\r\n\t\t{\r\n\t\t\tGeneratePerCellFiles( output_path );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tstring rawfile = Path.Combine( output_path, $\"TerrainGenerationUtility_export_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.raw\" );\r\n\t\tstring previewfile = Path.Combine( output_path, $\"TerrainGenerationUtility_preview_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png\" );\r\n\t\tstring splatfile = Path.Combine( output_path, $\"TerrainGenerationUtility_splat_export_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png\" );\r\n\r\n\t\t//Export RAW HeightMap file\r\n\t\tSaveRaw( _heightmap, rawfile );\r\n\t\tLog.Info( $\"Raw file generated! - {rawfile}\" );\r\n\t\t//Generate & Export Preview image for widget\r\n\t\tSKBitmap image = HeightmapToBitMap( _heightmap );\r\n\t\tSaveImage( image, previewfile );\r\n\t\tLog.Info( $\"HeightMap preview file generated! - {previewfile}\" );\r\n\t\t//Generate & Export SplatMap image\r\n\t\tfloat[,] splatmap = BuildTileGridSplatmap( _heightmap, TerrainGridSize,\r\n\t\t\t(int[])_tileSplatLayerCounts.Clone(), (SplatDispersionMode[])_tileSplatDispersions.Clone(), (float[])_tileSplatBlendStrengths.Clone() );\r\n\t\tSKBitmap splat = SplatmapToBitMap( splatmap, _splatcolors );\r\n\t\tSaveSplatmapAsPng( splat, splatfile );\r\n\t\tLog.Info( $\"Splatmap file generated! - {splatfile}\" );\r\n\r\n\t\t// Split the layers across the requested number of splat maps (based on the first tile's settings)\r\n\t\tint layerCount = _tileSplatLayerCounts != null && _tileSplatLayerCounts.Length > 0 ? Math.Max( _tileSplatLayerCounts[0], 2 ) : 2;\r\n\t\tint mapCount = _tileSplatMapCounts != null && _tileSplatMapCounts.Length > 0 ? Math.Max( _tileSplatMapCounts[0], 1 ) : 1;\r\n\t\tfor ( int m = 0; m < mapCount; m++ )\r\n\t\t{\r\n\t\t\tint startLayer = m * layerCount / mapCount;\r\n\t\t\tint endLayer = (m + 1) * layerCount / mapCount;\r\n\r\n\t\t\tvar mapBitmap = new SKBitmap( splatmap.GetLength( 0 ), splatmap.GetLength( 1 ) );\r\n\r\n\t\t\tfor ( int y = 0; y < mapBitmap.Height; y++ )\r\n\t\t\t{\r\n\t\t\t\tfor ( int x = 0; x < mapBitmap.Width; x++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat layerPos = Math.Clamp( splatmap[x, y], 0f, layerCount - 1f );\r\n\t\t\t\t\tint layer = (int)MathF.Round( layerPos );\r\n\r\n\t\t\t\t\tif ( layer >= startLayer && layer < endLayer )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat local = (layer - startLayer) / (float)Math.Max( endLayer - startLayer, 1 );\r\n\t\t\t\t\t\tvar color = SplatMapGradient.Evaluate( Math.Clamp( local, 0f, 1f ) ).ToColor32();\r\n\t\t\t\t\t\tmapBitmap.SetPixel( x, y, new SKColor( color.r, color.g, color.b, color.a ) );\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmapBitmap.SetPixel( x, y, new SKColor( 0, 0, 0, 255 ) );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tstring mapFile = Path.Combine( output_path, $\"TerrainGenerationUtility_splatmap_{m}_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png\" );\r\n\t\t\tSaveSplatmapAsPng( mapBitmap, mapFile );\r\n\t\t\tLog.Info( $\"Splatmap {m} file generated! - {mapFile}\" );\r\n\t\t}\r\n\r\n\t\tLog.Info( $\"All export files saved! {output_path}\" );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Exports each grid cell as its own full-resolution .raw heightmap and .png splatmap,\r\n\t/// named by grid coordinate.\r\n\t/// </summary>\r\n\tprivate void GeneratePerCellFiles( string output_path )\r\n\t{\r\n\t\tDirectory.CreateDirectory( output_path );\r\n\r\n\t\tint grid = Math.Max( TerrainGridSize, 1 );\r\n\r\n\t\tfor ( int ty = 0; ty < grid; ty++ )\r\n\t\t{\r\n\t\t\tfor ( int tx = 0; tx < grid; tx++ )\r\n\t\t\t{\r\n\t\t\t\tint index = ty * grid + tx;\r\n\t\t\t\tif ( index >= _cellHeightmaps.Count ) continue;\r\n\r\n\t\t\t\tvar heightmap = _cellHeightmaps[index];\r\n\r\n\t\t\t\tstring rawfile = Path.Combine( output_path, $\"TerrainGenerationUtility_cell_{tx}_{ty}.raw\" );\r\n\t\t\t\tSaveRaw( heightmap, rawfile );\r\n\t\t\t\tLog.Info( $\"Cell {tx},{ty} raw file generated! - {rawfile}\" );\r\n\r\n\t\t\t\tSKBitmap image = HeightmapToBitMap( heightmap );\r\n\t\t\t\tstring previewfile = Path.Combine( output_path, $\"TerrainGenerationUtility_cell_{tx}_{ty}_preview.png\" );\r\n\t\t\t\tSaveImage( image, previewfile );\r\n\t\t\t\tLog.Info( $\"Cell {tx},{ty} preview generated! - {previewfile}\" );\r\n\r\n\t\t\t\tif ( _cellSplatmaps != null && index < _cellSplatmaps.Count )\r\n\t\t\t\t{\r\n\t\t\t\t\tSKBitmap splat = SplatmapToBitMap( _cellSplatmaps[index], _splatcolors );\r\n\t\t\t\t\tstring splatfile = Path.Combine( output_path, $\"TerrainGenerationUtility_cell_{tx}_{ty}_splat.png\" );\r\n\t\t\t\t\tSaveSplatmapAsPng( splat, splatfile );\r\n\t\t\t\t\tLog.Info( $\"Cell {tx},{ty} splatmap generated! - {splatfile}\" );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tLog.Info( $\"All per-cell export files saved! {output_path}\" );\r\n\t}\r\n\r\n\tpublic float[,] GenerateHeightmap( int width, int height, Func<int, int, float> generator, float maxHeight, int smoothpasses )\r\n\t{\r\n\t\tfloat[,] heightmap = new float[width, height];\r\n\t\tfloat actualMaxHeight = float.MinValue;\r\n\r\n\t\t// Use parallel processing to generate heightmap\r\n\t\tobject maxLock = new object(); // Lock object for thread safety\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat value = generator( x, y );\r\n\t\t\t\theightmap[x, y] = value;\r\n\r\n\t\t\t\t// Update actual max height (thread-safe)\r\n\t\t\t\tlock ( maxLock )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( value > actualMaxHeight )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tactualMaxHeight = value;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t// Scale all values by the actual max height and up to the specified max height\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\theightmap[x, y] = (heightmap[x, y] / actualMaxHeight) * maxHeight;\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t// Apply smoothing if needed\r\n\t\tif ( smoothpasses > 0 )\r\n\t\t{\r\n\t\t\treturn SmoothHeightmap( heightmap, smoothpasses );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\treturn heightmap;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static float[,] GenerateStackedNoise(\r\n\t\tint width,\r\n\t\tint height,\r\n\t\tlong seed,\r\n\t\tint layers,\r\n\t\tfloat initialFrequency,\r\n\t\tfloat frequencyMultiplier,\r\n\t\tfloat initialAmplitude,\r\n\t\tfloat amplitudeMultiplier,\r\n\t\tFunc<int, int, float> shapeFunction, // Shape function applied after stacking noise\r\n\t\tfloat maxHeight,\r\n\t\tint smoothingPasses,\r\n\t\tfloat terrainPlaneScale // New variable to scale the noise\r\n\r\n\t)\r\n\t{\r\n\t\t// Initialize the heightmap with zeros\r\n\t\tfloat[,] heightmap = new float[width, height];\r\n\r\n\t\t// Random offset generator for noise layers\r\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\r\n\t\tfloat[] xOffsets = new float[layers];\r\n\t\tfloat[] yOffsets = new float[layers];\r\n\r\n\t\tfor ( int i = 0; i < layers; i++ )\r\n\t\t{\r\n\t\t\txOffsets[i] = random.Next( -100000, 100000 ) / 1000.0f;\r\n\t\t\tyOffsets[i] = random.Next( -100000, 100000 ) / 1000.0f;\r\n\t\t}\r\n\r\n\t\t// Adjust frequency based on TerrainPlaneScale\r\n\t\tfloat scaleFactor = Math.Clamp( terrainPlaneScale, 0.01f, 1.0f );\r\n\r\n\t\t// Multithreaded noise generation\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat value = 0.0f;\r\n\r\n\t\t\t\tfor ( int layer = 0; layer < layers; layer++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat frequency = initialFrequency * MathF.Pow( frequencyMultiplier, layer ) / scaleFactor;\r\n\t\t\t\t\tfloat amplitude = initialAmplitude * MathF.Pow( amplitudeMultiplier, layer );\r\n\r\n\t\t\t\t\t// Normalized coordinates adjusted by scale factor\r\n\t\t\t\t\tfloat nx = (x / (float)width) * frequency;\r\n\t\t\t\t\tfloat ny = (y / (float)height) * frequency;\r\n\r\n\t\t\t\t\t// Apply random offsets\r\n\t\t\t\t\tnx += xOffsets[layer];\r\n\t\t\t\t\tny += yOffsets[layer];\r\n\r\n\t\t\t\t\t// Generate noise\r\n\t\t\t\t\tfloat noiseValue = OpenSimplex2S.Noise2( (seed + layer) & 0xFFFFFFFF, nx, ny );\r\n\t\t\t\t\tvalue += Math.Clamp( noiseValue, -1.0f, 1.0f ) * amplitude;\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// Save the computed value to the heightmap\r\n\t\t\t\t// (each Parallel.For iteration writes its own distinct row - no lock needed)\r\n\t\t\t\theightmap[x, y] += value;\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t// Normalize the heightmap to the range [0, 1]\r\n\t\theightmap = NormalizeHeightmap( heightmap );\r\n\r\n\t\t// Apply the shape function and amplify its contribution if needed\r\n\t\tfloat shapeAmplification = 1.2f; // Adjust for stronger shape effects\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\theightmap[x, y] *= MathF.Pow( shapeFunction( x, y ), shapeAmplification );\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t// Rescale the heightmap to the desired maxHeight\r\n\t\tfloat currentMax = FindMaxHeight( heightmap );\r\n\t\tif ( currentMax > 0 )\r\n\t\t{\r\n\t\t\tParallel.For( 0, height, y =>\r\n\t\t\t{\r\n\t\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t\t{\r\n\t\t\t\t\theightmap[x, y] = (heightmap[x, y] / currentMax) * maxHeight;\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\r\n\t\t// Apply smoothing\r\n\t\tif ( smoothingPasses > 0 )\r\n\t\t{\r\n\t\t\theightmap = SmoothHeightmap( heightmap, smoothingPasses );\r\n\t\t}\r\n\r\n\t\treturn heightmap;\r\n\t}\r\n\r\n\r\n\t// Helper method to find the maximum height in a heightmap\r\n\tprivate static float FindMaxHeight( float[,] heightmap )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\r\n\t\tfloat max = float.MinValue;\r\n\t\tobject maxLock = new object();\r\n\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfloat rowMax = float.MinValue;\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tif ( heightmap[x, y] > rowMax )\r\n\t\t\t\t{\r\n\t\t\t\t\trowMax = heightmap[x, y];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( rowMax > max )\r\n\t\t\t{\r\n\t\t\t\tlock ( maxLock )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( rowMax > max ) max = rowMax;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\treturn max;\r\n\t}\r\n\r\n\r\n\tprivate static float[,] NormalizeHeightmap( float[,] heightmap )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\r\n\t\t// Find the min and max values (parallel, per-row reduce)\r\n\t\tobject lockObject = new object();\r\n\t\tfloat min = float.MaxValue;\r\n\t\tfloat max = float.MinValue;\r\n\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfloat rowMin = float.MaxValue;\r\n\t\t\tfloat rowMax = float.MinValue;\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat value = heightmap[x, y];\r\n\t\t\t\tif ( value < rowMin ) rowMin = value;\r\n\t\t\t\tif ( value > rowMax ) rowMax = value;\r\n\t\t\t}\r\n\r\n\t\t\tlock ( lockObject )\r\n\t\t\t{\r\n\t\t\t\tif ( rowMin < min ) min = rowMin;\r\n\t\t\t\tif ( rowMax > max ) max = rowMax;\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\tfloat range = max - min;\r\n\t\tif ( range <= 0f ) range = 1f;\r\n\r\n\t\t// Normalize the values (parallel, independent writes)\r\n\t\tfloat[,] normalized = new float[width, height];\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tnormalized[x, y] = (heightmap[x, y] - min) / range;\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\treturn normalized;\r\n\t}\r\n\r\n\tpublic static float[,] AddTurbulenceForRivers(\r\n\t\tfloat[,] heightmap,\r\n\t\tlong seed,\r\n\t\tfloat riverFrequency, // Frequency for river placement\r\n\t\tfloat riverWidth, // Width of the rivers\r\n\t\tfloat riverDepth, // Depth of the rivers\r\n\t\tfloat turbulenceFrequency, // Turbulence frequency\r\n\t\tfloat turbulenceStrength, // Turbulence strength\r\n\t\tfloat minRiverSpacing, // Minimum spacing between rivers\r\n\t\tfloat slopeSteepness, // Controls the gradual slope of the riverbanks\r\n\t\tfloat terrainNoiseFrequency, // Matches terrain surface noise frequency\r\n\t\tfloat terrainNoiseAmplitude // Matches terrain surface noise amplitude\r\n\t)\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\t\tfloat[,] newHeightmap = (float[,])heightmap.Clone();\r\n\r\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\r\n\t\tfloat[,] riverPlacementNoise = new float[width, height];\r\n\r\n\t\t// Generate river placement noise\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat nx = x / (float)width;\r\n\t\t\t\tfloat ny = y / (float)height;\r\n\r\n\t\t\t\t// Noise for river placement\r\n\t\t\t\triverPlacementNoise[x, y] = OpenSimplex2S.Noise2( seed, nx * riverFrequency, ny * riverFrequency );\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t// Process heightmap with river carving\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat nx = x / (float)width;\r\n\t\t\t\tfloat ny = y / (float)height;\r\n\r\n\t\t\t\tfloat riverNoise = MathF.Abs( riverPlacementNoise[x, y] ); // Use absolute noise for placement\r\n\r\n\t\t\t\t// Determine if the point is within the river carving zone\r\n\t\t\t\tif ( riverNoise < riverWidth )\r\n\t\t\t\t{\r\n\t\t\t\t\t// Calculate the smooth curve effect based on distance from the center\r\n\t\t\t\t\tfloat distanceFactor = 1.0f - (riverNoise / riverWidth); // 1 at center, 0 at edge\r\n\t\t\t\t\tfloat smoothDepthReduction = MathF.Pow( distanceFactor, slopeSteepness ) * riverDepth;\r\n\r\n\t\t\t\t\t// Add turbulence for a more organic flow\r\n\t\t\t\t\tfloat turbulence = OpenSimplex2S.Noise2( seed + 1, nx * turbulenceFrequency, ny * turbulenceFrequency )\r\n\t\t\t\t\t\t\t\t\t * turbulenceStrength;\r\n\r\n\t\t\t\t\t// Apply smooth depth reduction and turbulence\r\n\t\t\t\t\tfloat reducedHeight = newHeightmap[x, y] - smoothDepthReduction + turbulence;\r\n\r\n\t\t\t\t\t// Clamp height to ensure it doesn't rise above the original\r\n\t\t\t\t\tnewHeightmap[x, y] = MathF.Max( 0, MathF.Min( newHeightmap[x, y], reducedHeight ) );\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// Enforce minimum spacing between rivers\r\n\t\t\t\tif ( riverNoise < minRiverSpacing )\r\n\t\t\t\t{\r\n\t\t\t\t\t// Slightly raise the terrain to enforce separation\r\n\t\t\t\t\tnewHeightmap[x, y] += (minRiverSpacing - riverNoise) * 0.05f;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t// Add base noise to the entire heightmap after carving\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat nx = x / (float)width;\r\n\t\t\t\tfloat ny = y / (float)height;\r\n\r\n\t\t\t\t// Generate base noise\r\n\t\t\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed + 2, nx * 6f, ny * 6f )\r\n\t\t\t\t\t\t\t\t * 0.02f;\r\n\r\n\t\t\t\t// Add noise to the heightmap\r\n\t\t\t\tnewHeightmap[x, y] = MathF.Max( 0, newHeightmap[x, y] + baseNoise );\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\treturn newHeightmap;\r\n\t}\r\n\r\n\t// Smooths the heightmap using a simple box blur with adjustable strength\r\n\tprivate static float[,] SmoothHeightmap( float[,] heightmap, int smoothingPasses )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\t\tfloat[,] smoothed = new float[width, height];\r\n\r\n\t\tfor ( int pass = 0; pass < smoothingPasses; pass++ )\r\n\t\t{\r\n\t\t\tParallel.For( 0, height, y =>\r\n\t\t\t{\r\n\t\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat sum = 0;\r\n\t\t\t\t\tint count = 0;\r\n\r\n\t\t\t\t\t// Iterate through neighbors\r\n\t\t\t\t\tfor ( int dy = -1; dy <= 1; dy++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfor ( int dx = -1; dx <= 1; dx++ )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tint nx = x + dx;\r\n\t\t\t\t\t\t\tint ny = y + dy;\r\n\r\n\t\t\t\t\t\t\tif ( nx >= 0 && nx < width && ny >= 0 && ny < height )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tsum += heightmap[nx, ny];\r\n\t\t\t\t\t\t\t\tcount++;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tsmoothed[x, y] = sum / count;\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\r\n\t\t\t// Copy smoothed values back to the original heightmap for the next pass\r\n\t\t\tParallel.For( 0, height, y =>\r\n\t\t\t{\r\n\t\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t\t{\r\n\t\t\t\t\theightmap[x, y] = smoothed[x, y];\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\treturn smoothed;\r\n\t}\r\n\r\n\tpublic static ushort[] ConvertFloatArrayToUShortArray( float[,] input, float scale = 65535.0f )\r\n\t{\r\n\t\t// Get the dimensions of the 2D array\r\n\t\tint rows = input.GetLength( 0 );\r\n\t\tint cols = input.GetLength( 1 );\r\n\r\n\t\t// Initialize the 1D ushort array\r\n\t\tushort[] output = new ushort[rows * cols];\r\n\r\n\t\t// Iterate over the 2D array row by row\r\n\t\tint index = 0;\r\n\t\tfor ( int row = 0; row < rows; row++ )\r\n\t\t{\r\n\t\t\tfor ( int col = 0; col < cols; col++ )\r\n\t\t\t{\r\n\t\t\t\t// Convert the float to ushort, scaling if necessary\r\n\t\t\t\tfloat value = input[row, col];\r\n\t\t\t\tvalue = Math.Clamp( value, 0.0f, 1.0f ); // Ensure the float is in the 0 to 1 range\r\n\t\t\t\toutput[index++] = (ushort)(value * scale);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn output;\r\n\t}\r\n\r\n\tpublic static byte[] ConvertRawFloatArrayToByteArray( float[,] rawData, float scale = 65535.0f )\r\n\t{\r\n\t\tif ( rawData == null )\r\n\t\t{\r\n\t\t\tthrow new ArgumentNullException( nameof( rawData ), \"Input rawData cannot be null.\" );\r\n\t\t}\r\n\r\n\t\tint rows = rawData.GetLength( 0 );\r\n\t\tint cols = rawData.GetLength( 1 );\r\n\r\n\t\t// Create a byte array with 2 bytes per value\r\n\t\tbyte[] byteArray = new byte[rows * cols * 2]; // 2 bytes per ushort\r\n\r\n\t\tint index = 0;\r\n\t\tfor ( int row = 0; row < rows; row++ )\r\n\t\t{\r\n\t\t\tfor ( int col = 0; col < cols; col++ )\r\n\t\t\t{\r\n\t\t\t\tfloat value = rawData[row, col];\r\n\t\t\t\tvalue = Math.Clamp( value, 0.0f, 1.0f ); // Ensure value is in the range [0, 1]\r\n\r\n\t\t\t\t// Convert to 16-bit unsigned integer\r\n\t\t\t\tushort ushortValue = (ushort)(value * scale);\r\n\r\n\t\t\t\t// Store in byte array (little-endian order)\r\n\t\t\t\tbyteArray[index++] = (byte)(ushortValue & 0xFF); // Lower byte\r\n\t\t\t\tbyteArray[index++] = (byte)((ushortValue >> 8) & 0xFF); // Upper byte\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn byteArray;\r\n\t}\r\n\r\n\t// Converts a heightmap to a grayscale image using SkiaSharp\r\n\tpublic static SKBitmap HeightmapToBitMap( float[,] heightmap )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\t\tSKBitmap bitmap = new SKBitmap( width, height );\r\n\r\n\t\tfor ( int y = 0; y < height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tint intensity = (int)(heightmap[x, y] * 255);\r\n\t\t\t\tintensity = Math.Clamp( intensity, 0, 255 );\r\n\t\t\t\tbitmap.SetPixel( x, y, new SKColor( (byte)intensity, (byte)intensity, (byte)intensity ) );\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn bitmap;\r\n\t}\r\n\r\n\tpublic byte[] ConvertSKBitmapToBytes( SKBitmap bitmap, SKEncodedImageFormat format, int quality = 100 )\r\n\t{\r\n\t\t// Create an SKImage from the SKBitmap\r\n\t\tusing ( var image = SKImage.FromBitmap( bitmap ) )\r\n\t\t{\r\n\t\t\t// Encode the image to the desired format (e.g., PNG, JPEG)\r\n\t\t\tusing ( var data = image.Encode( format, quality ) )\r\n\t\t\t{\r\n\t\t\t\t// Convert SKData to a byte array\r\n\t\t\t\treturn data.ToArray();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static void SaveImage(\r\n\tSKBitmap bitmap,\r\n\tstring filename,\r\n\tfloat rotationDegrees = 270f,\r\n\tbool reverseHorizontal = false,\r\n\tbool reverseVertical = true\r\n)\r\n\t{\r\n\t\tint width = bitmap.Width;\r\n\t\tint height = bitmap.Height;\r\n\r\n\t\t// Create a new bitmap to hold the transformed image\r\n\t\tusing var transformedBitmap = new SKBitmap( width, height );\r\n\r\n\t\t// Create a canvas to draw the transformed image\r\n\t\tusing var canvas = new SKCanvas( transformedBitmap );\r\n\r\n\t\t// Clear the canvas with transparency\r\n\t\tcanvas.Clear( SKColors.Transparent );\r\n\r\n\t\t// Apply transformations\r\n\t\tcanvas.Save();\r\n\r\n\t\t// Translate to the center of the canvas for rotation and flipping\r\n\t\tcanvas.Translate( width / 2f, height / 2f );\r\n\r\n\t\t// Apply flipping first\r\n\t\tfloat scaleX = reverseHorizontal ? -1f : 1f;\r\n\t\tfloat scaleY = reverseVertical ? -1f : 1f;\r\n\t\tcanvas.Scale( scaleX, scaleY );\r\n\r\n\t\t// Apply rotation\r\n\t\tif ( rotationDegrees != 0 )\r\n\t\t{\r\n\t\t\tcanvas.RotateDegrees( rotationDegrees );\r\n\t\t}\r\n\r\n\t\t// Translate back to ensure the image is drawn correctly\r\n\t\tcanvas.Translate( -width / 2f, -height / 2f );\r\n\r\n\t\t// Draw the original bitmap onto the transformed canvas\r\n\t\tcanvas.DrawBitmap( bitmap, 0, 0 );\r\n\r\n\t\t// Restore the canvas to finalize the transformations\r\n\t\tcanvas.Restore();\r\n\r\n\t\t// Flush the canvas\r\n\t\tcanvas.Flush();\r\n\r\n\t\t// Save the transformed bitmap as a PNG file\r\n\t\tusing var pixmap = transformedBitmap.PeekPixels();\r\n\t\tusing var image = SKImage.FromPixels( pixmap );\r\n\t\tusing var data = image.Encode( SKEncodedImageFormat.Png, 100 );\r\n\r\n\t\tusing var stream = File.OpenWrite( filename );\r\n\t\tdata.SaveTo( stream );\r\n\t}\r\n\r\n\tpublic static void SaveRaw( float[,] heightmap, string filename, int rotationDegrees = 270, bool reverseHorizontal = true, bool reverseVertical = false )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\r\n\t\t// Rotate the heightmap if requested\r\n\t\tif ( rotationDegrees != 0 )\r\n\t\t{\r\n\t\t\theightmap = RotateHeightmap( heightmap, rotationDegrees );\r\n\t\t\tif ( rotationDegrees == 90 || rotationDegrees == 270 )\r\n\t\t\t{\r\n\t\t\t\t// Swap width and height for 90\u00b0 or 270\u00b0 rotations\r\n\t\t\t\t(width, height) = (height, width);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Reverse the heightmap if requested\r\n\t\tif ( reverseHorizontal || reverseVertical )\r\n\t\t{\r\n\t\t\theightmap = ReverseHeightmap( heightmap, reverseHorizontal, reverseVertical );\r\n\t\t}\r\n\r\n\t\tusing var fileStream = new FileStream( filename, FileMode.Create, FileAccess.Write );\r\n\t\tusing var binaryWriter = new BinaryWriter( fileStream );\r\n\r\n\t\tfor ( int y = 0; y < height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\t// Scale image data to 16-bit\r\n\t\t\t\tushort value = (ushort)(Math.Clamp( heightmap[x, y], 0, 1 ) * 65535);\r\n\t\t\t\tbinaryWriter.Write( value );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Helper method to rotate the heightmap by 90\u00b0, 180\u00b0, or 270\u00b0\r\n\tprivate static float[,] RotateHeightmap( float[,] original, int rotationDegrees )\r\n\t{\r\n\t\tint originalWidth = original.GetLength( 0 );\r\n\t\tint originalHeight = original.GetLength( 1 );\r\n\r\n\t\tfloat[,] rotated;\r\n\r\n\t\tswitch ( rotationDegrees )\r\n\t\t{\r\n\t\t\tcase 90:\r\n\t\t\t\trotated = new float[originalHeight, originalWidth];\r\n\t\t\t\tfor ( int y = 0; y < originalHeight; y++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfor ( int x = 0; x < originalWidth; x++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\trotated[y, originalWidth - 1 - x] = original[x, y];\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase 180:\r\n\t\t\t\trotated = new float[originalWidth, originalHeight];\r\n\t\t\t\tfor ( int y = 0; y < originalHeight; y++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfor ( int x = 0; x < originalWidth; x++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\trotated[originalWidth - 1 - x, originalHeight - 1 - y] = original[x, y];\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase 270:\r\n\t\t\t\trotated = new float[originalHeight, originalWidth];\r\n\t\t\t\tfor ( int y = 0; y < originalHeight; y++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tfor ( int x = 0; x < originalWidth; x++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\trotated[originalHeight - 1 - y, x] = original[x, y];\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tdefault:\r\n\t\t\t\tthrow new ArgumentException( \"Rotation must be 0, 90, 180, or 270 degrees.\" );\r\n\t\t}\r\n\r\n\t\treturn rotated;\r\n\t}\r\n\r\n\t// Helper method to reverse the heightmap horizontally and/or vertically\r\n\tprivate static float[,] ReverseHeightmap( float[,] original, bool reverseHorizontal, bool reverseVertical )\r\n\t{\r\n\t\tint width = original.GetLength( 0 );\r\n\t\tint height = original.GetLength( 1 );\r\n\r\n\t\tfloat[,] reversed = new float[width, height];\r\n\r\n\t\tfor ( int y = 0; y < height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tint targetX = reverseHorizontal ? width - 1 - x : x;\r\n\t\t\t\tint targetY = reverseVertical ? height - 1 - y : y;\r\n\t\t\t\treversed[targetX, targetY] = original[x, y];\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn reversed;\r\n\t}\r\n\r\n\tpublic static float[,] GenerateSplatmap( float[,] heightmap, float[] thresholds, float maxHeight, int layerCount = -1, SplatDispersionMode dispersion = SplatDispersionMode.Evenly, float blendStrength = 0.35f )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\r\n\t\tint layers = layerCount > 0 ? layerCount : Math.Max( thresholds.Length, 2 );\r\n\t\tfloat[,] splatmap = new float[width, height];\r\n\r\n\t\t// Build normalized height bounds from the data itself (robust to maxHeight being lower than peaks)\r\n\t\tfloat minH = float.MaxValue, maxH = float.MinValue;\r\n\t\tobject minMaxLock = new object();\r\n\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfloat rowMin = float.MaxValue;\r\n\t\t\tfloat rowMax = float.MinValue;\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tif ( heightmap[x, y] < rowMin ) rowMin = heightmap[x, y];\r\n\t\t\t\tif ( heightmap[x, y] > rowMax ) rowMax = heightmap[x, y];\r\n\t\t\t}\r\n\r\n\t\t\tlock ( minMaxLock )\r\n\t\t\t{\r\n\t\t\t\tif ( rowMin < minH ) minH = rowMin;\r\n\t\t\t\tif ( rowMax > maxH ) maxH = rowMax;\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\tfloat range = MathF.Max( maxH - minH, 0.0001f );\r\n\r\n\t\t// Thresholds are the height positions of each color stop in [0,1].\r\n\t\t// Evenly mode uses equally spaced stops; Natural mode uses a slope-weighted\r\n\t\t// distribution so colors bunch on flat/common terrain and spread on steep slopes.\r\n\t\tfloat[] stops = thresholds;\r\n\t\tif ( dispersion == SplatDispersionMode.Natural )\r\n\t\t{\r\n\t\t\tstops = ComputeNaturalThresholds( heightmap, layers );\r\n\t\t}\r\n\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\t// Normalized height in [0,1]\r\n\t\t\t\tfloat normalizedHeight = Math.Clamp( (heightmap[x, y] - minH) / range, 0f, 1f );\r\n\r\n\t\t\t\t// Interpolated layer position from the color stop positions\r\n\t\t\t\tfloat layerPos = HeightToLayer( normalizedHeight, stops );\r\n\r\n\t\t\t\t// Soft snap to the nearest layer governed by blend strength\r\n\t\t\t\tfloat center = MathF.Round( layerPos );\r\n\t\t\t\tfloat distance = layerPos - center;\r\n\r\n\t\t\t\tfloat factor;\r\n\t\t\t\tif ( MathF.Abs( distance ) <= blendStrength * 0.5f )\r\n\t\t\t\t{\r\n\t\t\t\t\tfactor = layerPos;\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tfactor = center;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tsplatmap[x, y] = Math.Clamp( factor, 0f, layers - 1 );\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\treturn splatmap;\r\n\t}\r\n\r\n\tstatic float HeightToLayer( float normalizedHeight, float[] stops )\r\n\t{\r\n\t\tint count = stops.Length;\r\n\t\tif ( count <= 1 ) return 0f;\r\n\t\tif ( normalizedHeight <= stops[0] ) return 0f;\r\n\t\tif ( normalizedHeight >= stops[count - 1] ) return count - 1f;\r\n\r\n\t\tfor ( int i = 0; i < count - 1; i++ )\r\n\t\t{\r\n\t\t\tif ( normalizedHeight >= stops[i] && normalizedHeight <= stops[i + 1] )\r\n\t\t\t{\r\n\t\t\t\tfloat t = (normalizedHeight - stops[i]) / MathF.Max( stops[i + 1] - stops[i], 0.0001f );\r\n\t\t\t\treturn i + t;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn count - 1f;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Places color stop thresholds based on the terrain's slope-weighted height distribution.\r\n\t/// Flat, common heights get many stops (lots of color blending); steep, rare heights get few\r\n\t/// stops (few color changes), matching how terrain materials naturally appear.\r\n\t/// </summary>\r\n\tstatic float[] ComputeNaturalThresholds( float[,] heightmap, int layerCount )\r\n\t{\r\n\t\tint width = heightmap.GetLength( 0 );\r\n\t\tint height = heightmap.GetLength( 1 );\r\n\r\n\t\tfloat minH = float.MaxValue, maxH = float.MinValue;\r\n\t\tobject minMaxLock = new object();\r\n\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfloat rowMin = float.MaxValue;\r\n\t\t\tfloat rowMax = float.MinValue;\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tif ( heightmap[x, y] < rowMin ) rowMin = heightmap[x, y];\r\n\t\t\t\tif ( heightmap[x, y] > rowMax ) rowMax = heightmap[x, y];\r\n\t\t\t}\r\n\r\n\t\t\tlock ( minMaxLock )\r\n\t\t\t{\r\n\t\t\t\tif ( rowMin < minH ) minH = rowMin;\r\n\t\t\t\tif ( rowMax > maxH ) maxH = rowMax;\r\n\t\t\t}\r\n\t\t} );\r\n\t\tfloat range = MathF.Max( maxH - minH, 0.0001f );\r\n\r\n\t\t// Histogram of normalized heights, weighted by flatness (1 - slope).\r\n\t\t// Use a per-thread local histogram, then merge, to avoid lock contention.\r\n\t\tconst int bins = 128;\r\n\t\tfloat[] hist = new float[bins];\r\n\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfloat[] localHist = new float[bins];\r\n\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\tfloat h = heightmap[x, y];\r\n\r\n\t\t\t\tfloat hL = heightmap[Math.Max( x - 1, 0 ), y];\r\n\t\t\t\tfloat hR = heightmap[Math.Min( x + 1, width - 1 ), y];\r\n\t\t\t\tfloat hD = heightmap[x, Math.Max( y - 1, 0 )];\r\n\t\t\t\tfloat hU = heightmap[x, Math.Min( y + 1, height - 1 )];\r\n\r\n\t\t\t\tfloat localDiff = (MathF.Abs( hR - hL ) + MathF.Abs( hU - hD )) * 0.5f;\r\n\t\t\t\tfloat slope = Math.Clamp( localDiff / MathF.Max( range * 0.1f, 0.0001f ), 0f, 1f );\r\n\r\n\t\t\t\tfloat weight = MathF.Max( 1f - slope, 0.05f );\r\n\t\t\t\tfloat normalizedHeight = Math.Clamp( (h - minH) / range, 0f, 1f );\r\n\t\t\t\tint bin = Math.Clamp( (int)(normalizedHeight * (bins - 1)), 0, bins - 1 );\r\n\t\t\t\tlocalHist[bin] += weight;\r\n\t\t\t}\r\n\r\n\t\t\tlock ( minMaxLock )\r\n\t\t\t{\r\n\t\t\t\tfor ( int i = 0; i < bins; i++ ) hist[i] += localHist[i];\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\t// Cumulative distribution\r\n\t\tfloat total = hist.Sum();\r\n\t\tif ( total <= 0f )\r\n\t\t{\r\n\t\t\ttotal = 1f;\r\n\t\t\tfor ( int i = 0; i < bins; i++ ) hist[i] = 1f;\r\n\t\t}\r\n\r\n\t\tfloat[] thresholds = new float[layerCount];\r\n\t\tthresholds[0] = 0f;\r\n\t\tthresholds[layerCount - 1] = 1f;\r\n\r\n\t\tfloat cum = 0f;\r\n\t\tint binIndex = 0;\r\n\t\tfor ( int i = 1; i < layerCount - 1; i++ )\r\n\t\t{\r\n\t\t\tfloat target = (i / (float)(layerCount - 1)) * total;\r\n\t\t\twhile ( binIndex < bins - 1 && cum < target )\r\n\t\t\t{\r\n\t\t\t\tcum += hist[binIndex];\r\n\t\t\t\tbinIndex++;\r\n\t\t\t}\r\n\t\t\tthresholds[i] = binIndex / (float)(bins - 1);\r\n\t\t}\r\n\r\n\t\t// Ensure monotonic\r\n\t\tfor ( int i = 1; i < layerCount; i++ )\r\n\t\t{\r\n\t\t\tthresholds[i] = MathF.Max( thresholds[i], thresholds[i - 1] );\r\n\t\t}\r\n\r\n\t\treturn thresholds;\r\n\t}\r\n\r\n\tpublic static SKBitmap SplatmapToBitMap( float[,] splatmap, SKColor[] colors )\r\n\t{\r\n\t\tint width = splatmap.GetLength( 0 );\r\n\t\tint height = splatmap.GetLength( 1 );\r\n\t\tSKBitmap bitmap = new SKBitmap( width, height );\r\n\r\n\t\tParallel.For( 0, height, y =>\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < width; x++ )\r\n\t\t\t{\r\n\t\t\t\t// Map the splatmap value to a valid layer position\r\n\t\t\t\tfloat layerPos = Math.Clamp( splatmap[x, y], 0f, colors.Length - 1f );\r\n\t\t\t\tint layer0 = (int)MathF.Floor( layerPos );\r\n\t\t\t\tint layer1 = Math.Min( layer0 + 1, colors.Length - 1 );\r\n\t\t\t\tfloat t = layerPos - layer0;\r\n\r\n\t\t\t\t// Blend between the two nearest layer colors\r\n\t\t\t\tvar c0 = colors[layer0];\r\n\t\t\t\tvar c1 = colors[layer1];\r\n\t\t\t\tvar color = new SKColor(\r\n\t\t\t\t\t(byte)MathX.LerpTo( c0.Red, c1.Red, t ),\r\n\t\t\t\t\t(byte)MathX.LerpTo( c0.Green, c1.Green, t ),\r\n\t\t\t\t\t(byte)MathX.LerpTo( c0.Blue, c1.Blue, t ),\r\n\t\t\t\t\t(byte)MathX.LerpTo( c0.Alpha, c1.Alpha, t ) );\r\n\r\n\t\t\t\tbitmap.SetPixel( x, y, color );\r\n\t\t\t}\r\n\t\t} );\r\n\r\n\t\treturn bitmap;\r\n\t}\r\n\r\n\tpublic static void SaveSplatmapAsPng(\r\n\tSKBitmap bitmap,\r\n\tstring filename,\r\n\tfloat rotationDegrees = 270f,\r\n\tbool reverseHorizontal = false,\r\n\tbool reverseVertical = true\r\n)\r\n\t{\r\n\t\tint width = bitmap.Width;\r\n\t\tint height = bitmap.Height;\r\n\t\tusing var transformedBitmap = new SKBitmap( width, height );\r\n\t\tusing var canvas = new SKCanvas( transformedBitmap );\r\n\r\n\t\t// Clear the canvas with transparency\r\n\t\tcanvas.Clear( SKColors.Transparent );\r\n\t\t// Apply transformations\r\n\t\tcanvas.Save();\r\n\t\t// Translate to the center of the canvas for rotation and flipping\r\n\t\tcanvas.Translate( width / 2f, height / 2f );\r\n\t\t// Apply flipping first\r\n\t\tfloat scaleX = reverseHorizontal ? -1f : 1f;\r\n\t\tfloat scaleY = reverseVertical ? -1f : 1f;\r\n\t\tcanvas.Scale( scaleX, scaleY );\r\n\r\n\t\t// Apply rotation\r\n\t\tif ( rotationDegrees != 0 )\r\n\t\t{\r\n\t\t\tcanvas.RotateDegrees( rotationDegrees );\r\n\t\t}\r\n\r\n\t\t// Translate back to ensure the image is drawn correctly\r\n\t\tcanvas.Translate( -width / 2f, -height / 2f );\r\n\t\t// Draw the original bitmap onto the transformed canvas\r\n\t\tcanvas.DrawBitmap( bitmap, 0, 0 );\r\n\t\t// Restore the canvas to finalize the transformations\r\n\t\tcanvas.Restore();\r\n\t\t// Flush the canvas\r\n\t\tcanvas.Flush();\r\n\r\n\t\t// Save the transformed bitmap as a PNG file\r\n\t\tusing var pixmap = transformedBitmap.PeekPixels();\r\n\t\tusing var image = SKImage.FromPixels( pixmap );\r\n\t\tusing var data = image.Encode( SKEncodedImageFormat.Png, 100 );\r\n\r\n\t\tusing var stream = File.OpenWrite( filename );\r\n\t\tdata.SaveTo( stream );\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// An icon + label picker whose options wrap onto multiple lines.\r\n/// Mimics the interface of <see cref=\"Editor.SegmentedControl\"/> (AddOption, Selected, SelectedIndex, OnSelectedChanged).\r\n/// </summary>\r\npublic class WrapSelector : Widget\r\n{\r\n\treadonly List<WrapOption> _buttons = new();\r\n\treadonly List<string> _names = new();\r\n\r\n\tpublic string Selected\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i < _buttons.Count; i++ )\r\n\t\t\t{\r\n\t\t\t\tif ( _buttons[i].IsActive )\r\n\t\t\t\t\treturn _names[i];\r\n\t\t\t}\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\tset\r\n\t\t{\r\n\t\t\tSetSelected( value );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic int SelectedIndex\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i < _buttons.Count; i++ )\r\n\t\t\t{\r\n\t\t\t\tif ( _buttons[i].IsActive )\r\n\t\t\t\t\treturn i;\r\n\t\t\t}\r\n\t\t\treturn -1;\r\n\t\t}\r\n\t\tset\r\n\t\t{\r\n\t\t\tif ( value >= 0 && value < _names.Count )\r\n\t\t\t\tSetSelected( _names[value] );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic Action<string> OnSelectedChanged { get; set; }\r\n\r\n\tpublic WrapSelector( Widget parent = null ) : base( parent )\r\n\t{\r\n\t\tLayout = Layout.Row();\r\n\t\tLayout.Spacing = 4;\r\n\t\tSetSizeMode( SizeMode.CanGrow, SizeMode.CanGrow );\r\n\t\tHorizontalSizeMode = SizeMode.Flexible;\r\n\t}\r\n\r\n\tpublic void AddOption( string name, string icon = null, int? count = null, string label = null )\r\n\t{\r\n\t\tif ( _names.Contains( name ) ) return;\r\n\r\n\t\tif ( string.IsNullOrEmpty( name ) )\r\n\t\t{\r\n\t\t\t// Special \"clear\" option, shown with the close icon\r\n\t\t\ticon ??= \"close\";\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\ticon ??= IconFor( name );\r\n\t\t}\r\n\r\n\t\tvar option = new WrapOption( this, label ?? (string.IsNullOrEmpty( name ) ? \"Clear\" : name), icon );\r\n\t\toption.IsActive = false;\r\n\t\toption.MouseLeftPress = () => SetSelected( name );\r\n\r\n\t\t_names.Add( name );\r\n\t\t_buttons.Add( option );\r\n\t\tLayout.Add( option );\r\n\t}\r\n\r\n\tpublic bool HasOption( string name ) => _names.Contains( name );\r\n\r\n\tpublic new void DestroyChildren()\r\n\t{\r\n\t\tforeach ( var b in _buttons )\r\n\t\t{\r\n\t\t\tif ( b.IsValid() )\r\n\t\t\t\tb.Destroy();\r\n\t\t}\r\n\t\t_buttons.Clear();\r\n\t\t_names.Clear();\r\n\t}\r\n\r\n\tvoid SetSelected( string name )\r\n\t{\r\n\t\tbool changed = Selected != name;\r\n\r\n\t\tfor ( int i = 0; i < _buttons.Count; i++ )\r\n\t\t{\r\n\t\t\t_buttons[i].IsActive = _names[i] == name;\r\n\t\t}\r\n\r\n\t\tif ( changed )\r\n\t\t{\r\n\t\t\tOnSelectedChanged?.Invoke( name );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string IconFor( string name )\r\n\t{\r\n\t\tswitch ( name )\r\n\t\t{\r\n\t\t\tcase \"Islands\": return \"landscape\";\r\n\t\t\tcase \"Mountainous\": return \"terrain\";\r\n\t\t\tcase \"Planetary\": return \"public\";\r\n\t\t\tcase \"Realistic\": return \"photo\";\r\n\t\t\tcase \"Sea\": return \"water\";\r\n\t\t\tcase \"Volcanic\": return \"volcano\";\r\n\t\t\tcase \"Default\": return \"shapes\";\r\n\t\t\tcase \"Archipelagos\": return \"scatter_plot\";\r\n\t\t\tcase \"Atoll\": return \"crop_square\";\r\n\t\t\tcase \"Islets\": return \"blur_on\";\r\n\t\t\tcase \"Oceanic\": return \"waves\";\r\n\t\t\tcase \"Cliff\": return \"terrain\";\r\n\t\t\tcase \"Craters\": return \"brightness_low\";\r\n\t\t\tcase \"Hills\": return \"landscape\";\r\n\t\t\tcase \"Plateau\": return \"square_foot\";\r\n\t\t\tcase \"SeaBed\": return \"water\";\r\n\t\t\tcase \"Sharded\": return \"dashboard\";\r\n\t\t\tdefault: return \"shapes\";\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// A single option in a <see cref=\"WrapSelector\"/>: an icon with a text label underneath.\r\n/// </summary>\r\npublic class WrapOption : Widget\r\n{\r\n\tpublic string Icon { get; }\r\n\tpublic string Text { get; }\r\n\tpublic bool IsActive { get; set; }\r\n\r\n\tpublic WrapOption( Widget parent, string text, string icon ) : base( parent )\r\n\t{\r\n\t\tText = text;\r\n\t\tIcon = icon;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tToolTip = text;\r\n\t\tMinimumSize = new Vector2( 52, 44 );\r\n\t}\r\n\r\n\tprotected override Vector2 SizeHint()\r\n\t{\r\n\t\tPaint.SetDefaultFont( 7 );\r\n\t\tvar textRect = Paint.MeasureText( new Rect( 0, 0, 60, 100 ), Text, TextFlag.WordWrap );\r\n\t\treturn new Vector2( MathF.Max( textRect.Size.x + 10, 52 ), textRect.Size.y + 24 );\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\r\n\t\tvar rect = LocalRect;\r\n\r\n\t\tvar background = IsActive ? Theme.Primary.WithAlpha( 0.25f ) : Theme.ControlBackground.WithAlpha( 0.6f );\r\n\t\tif ( Paint.HasMouseOver ) background = background.Lighten( 0.1f );\r\n\t\tPaint.SetBrush( background );\r\n\t\tPaint.DrawRect( rect, Theme.ControlRadius );\r\n\r\n\t\tvar iconRect = new Rect( rect.Left, rect.Top + 4, rect.Width, rect.Height * 0.55f );\r\n\t\tvar color = IsActive ? Theme.Primary : Theme.Text.WithAlpha( 0.8f );\r\n\t\tPaint.SetPen( color );\r\n\t\tPaint.DrawIcon( iconRect, Icon, 18, TextFlag.Center );\r\n\r\n\t\tvar textRect = new Rect( rect.Left + 2, rect.Top + rect.Height * 0.55f, rect.Width - 4, rect.Height * 0.45f );\r\n\t\tPaint.SetDefaultFont( 7 );\r\n\t\tPaint.DrawText( textRect, Text, TextFlag.Center | TextFlag.WordWrap );\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// A clickable box representing one terrain grid cell (or the \"All\" box). Shows the tile's\r\n/// current category name and is highlighted when selected.\r\n/// </summary>\r\npublic class TileGridBox : Widget\r\n{\r\n\tpublic int Index { get; }\r\n\tpublic string Text { get; set; }\r\n\tstring _subtitle;\r\n\tpublic bool IsSelected { get; set; }\r\n\tpublic Action OnClicked { get; set; }\r\n\r\n\tpublic TileGridBox( Widget parent, int index, string text, string subtitle = null ) : base( parent )\r\n\t{\r\n\t\tIndex = index;\r\n\t\tText = text;\r\n\t\t_subtitle = subtitle;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tToolTip = subtitle ?? text;\r\n\t\tMinimumSize = new Vector2( 44, 40 );\r\n\t\tMouseLeftPress = () => OnClicked?.Invoke();\r\n\t}\r\n\r\n\tprotected override Vector2 SizeHint()\r\n\t{\r\n\t\treturn new Vector2( 48, 44 );\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\r\n\t\tvar rect = LocalRect;\r\n\t\tvar bg = IsSelected ? Theme.Primary.WithAlpha( 0.3f ) : Theme.ControlBackground.WithAlpha( 0.6f );\r\n\t\tif ( Paint.HasMouseOver ) bg = bg.Lighten( 0.1f );\r\n\t\tPaint.SetBrush( bg );\r\n\t\tPaint.DrawRect( rect, Theme.ControlRadius );\r\n\r\n\t\tif ( IsSelected )\r\n\t\t{\r\n\t\t\tPaint.SetPen( Theme.Primary, 2 );\r\n\t\t\tPaint.DrawRect( rect, Theme.ControlRadius );\r\n\t\t}\r\n\r\n\t\tvar textRect = new Rect( rect.Left + 3, rect.Top + 2, rect.Width - 6, rect.Height - 4 );\r\n\r\n\t\tPaint.SetDefaultFont( 7 );\r\n\t\tPaint.SetPen( IsSelected ? Theme.Primary : Theme.Text.WithAlpha( 0.9f ) );\r\n\t\tPaint.DrawText( textRect, Text ?? \"?\", TextFlag.Center | TextFlag.WordWrap );\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// A compact dropdown-style picker used in the tile grid. Shows a button with the current value\r\n/// and opens a popup listing the available options.\r\n/// </summary>\r\npublic class TileDropdownPicker : Widget\r\n{\r\n\tstring[] _options;\r\n\tButton _button;\r\n\tstring _label;\r\n\r\n\tpublic string Selected { get; private set; }\r\n\tpublic Action<string> OnPicked { get; set; }\r\n\r\n\tpublic TileDropdownPicker( Widget parent, string label, string[] options ) : base( parent )\r\n\t{\r\n\t\t_label = label;\r\n\t\t_options = options ?? Array.Empty<string>();\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Spacing = 2;\r\n\r\n\t\tvar labelWidget = new Label( label );\r\n\t\tlabelWidget.SetStyles( \"font-size: 9px; color: #999;\" );\r\n\t\tLayout.Add( labelWidget );\r\n\r\n\t\t_button = new Button( Selected ?? \"None\", this );\r\n\t\t_button.FixedHeight = Theme.RowHeight;\r\n\t\t_button.Clicked += OpenMenu;\r\n\t\tLayout.Add( _button );\r\n\t}\r\n\r\n\tpublic void SetOptions( string[] options )\r\n\t{\r\n\t\t_options = options ?? Array.Empty<string>();\r\n\t}\r\n\r\n\tpublic void SetSelected( string value )\r\n\t{\r\n\t\tSelected = value;\r\n\t\tif ( _button != null && _button.IsValid() )\r\n\t\t\t_button.Text = value ?? \"None\";\r\n\t}\r\n\r\n\tvoid OpenMenu()\r\n\t{\r\n\t\tvar popup = new PopupWidget( null );\r\n\t\tpopup.Layout = Layout.Column();\r\n\t\tpopup.Layout.Margin = 4;\r\n\t\tpopup.Width = Math.Max( 180, _button.ScreenRect.Width );\r\n\r\n\t\tvar scroller = popup.Layout.Add( new ScrollArea( this ), 1 );\r\n\t\tscroller.Canvas = new Widget( scroller )\r\n\t\t{\r\n\t\t\tLayout = Layout.Column(),\r\n\t\t\tVerticalSizeMode = SizeMode.CanGrow | SizeMode.Expand\r\n\t\t};\r\n\r\n\t\tforeach ( var option in _options )\r\n\t\t{\r\n\t\t\tvar item = scroller.Canvas.Layout.Add( new Button( option ) );\r\n\t\t\titem.MouseLeftPress = () =>\r\n\t\t\t{\r\n\t\t\t\tSetSelected( option );\r\n\t\t\t\tOnPicked?.Invoke( option );\r\n\t\t\t\tpopup.Close();\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tpopup.Position = _button.ScreenRect.BottomLeft;\r\n\t\tpopup.Visible = true;\r\n\t\tpopup.AdjustSize();\r\n\t\tpopup.ConstrainToScreen();\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Theme.ControlBackground );\r\n\t\tPaint.DrawRect( LocalRect, Theme.ControlRadius );\r\n\t\tbase.OnPaint();\r\n\t}\r\n}\r\n\r\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/TerrainShapes/Realistic.cs",
"FileName": "Realistic.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using Editor;\nusing Sandbox;\nusing System;\n\nnamespace Sturnus.TerrainGenerationTool;\npublic static class Realistic\n{\n\tpublic static float Default(\n\tint x,\n\tint y,\n\tint width,\n\tint height,\n\tlong seed,\n\tfloat minHeight,\n\tbool domainWarping,\n\tfloat domainWarpingSize,\n\tfloat domainWarpingStrength)\n\t{\n\t\t// Normalize coordinates to [-1, 1]\n\t\tfloat nx = (x / (float)width) * 2 - 1;\n\t\tfloat ny = (y / (float)height) * 2 - 1;\n\n\t\t// Apply domain warping for natural distortion\n\t\tif ( domainWarping )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed, nx * domainWarpingSize, ny * domainWarpingSize ) * domainWarpingStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 1, nx * domainWarpingSize, ny * domainWarpingSize ) * domainWarpingStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\tfloat baseTerrain = OpenSimplex2S.Noise2( seed, nx, ny );\n\n\t\t// Create hill/valley transitions with non-linear blending\n\t\tfloat hillFactor = MathF.Pow( baseTerrain, 6 ); // Emphasize hills\n\t\tfloat valleyFactor = 1.0f - MathF.Pow( 1.0f - baseTerrain, 1 ); // Emphasize valleys\n\n\t\t// Use smoothstep-like function for non-linear blending\n\t\tfloat smoothTransition = SmoothStep( 0.75f, 1.25f, baseTerrain );\n\t\tfloat terrainShape = MathX.Lerp( valleyFactor, hillFactor, smoothTransition );\n\n\t\t// Add finer details\n\t\tfloat fineNoise = OpenSimplex2S.Noise2( seed + 2, nx * 8.0f, ny * 8.0f ) * 0.1f;\n\n\t\t// Combine base terrain with fine details\n\t\tfloat heightValue = terrainShape + fineNoise;\n\n\t\t// Add a baseline value to ensure no flat zero areas\n\t\tfloat baseline = minHeight; // Minimum height\n\t\theightValue = MathF.Max( heightValue, baseline );\n\n\t\t// Normalize height to [0, 1]\n\t\treturn Math.Clamp( heightValue, 0.0f, 1.0f );\n\t}\n\n\tpublic static float Hills( int x, int y, int width, int height, long seed, float minHeight, bool warp, float warpSize = 0.1f, float warpStrength = 0.5f )\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\n\t\tfloat hillHeight = 0.6f; // Maximum height of the hills\n\t\tfloat hillFrequency = 3f; // Frequency of the hills\n\t\tfloat noiseStrength = 0f; // Strength of noise for natural detail\n\n\t\t// Apply domain warping for irregularity\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Base overlapping hills\n\t\tfloat hillBase1 = MathF.Sin( nx * hillFrequency * MathF.PI ) + MathF.Cos( ny * hillFrequency * MathF.PI );\n\t\tfloat hillBase2 = MathF.Sin( ny * (hillFrequency * 0.75f) * MathF.PI ) + MathF.Cos( nx * (hillFrequency * 0.75f) * MathF.PI );\n\t\tfloat combinedHills = (hillBase1 + hillBase2) / 5f; // Blend two hill patterns\n\t\tcombinedHills = MathF.Abs( combinedHills ); // Ensure positive-only values\n\n\t\t// Add noise for natural detail\n\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed, nx * 6.0f, ny * 6.0f ) * noiseStrength;\n\t\tfloat fineNoise = OpenSimplex2S.Noise2( seed + 1, nx * 12.0f, ny * 12.0f ) * (noiseStrength / 2);\n\n\t\t// Combine hills and noise\n\t\tfloat heightValue = (combinedHills * hillHeight) + minHeight + fineNoise;\n\t\t// Ensure minimum base height\n\t\n\t\t// Clamp the final height value\n\t\treturn Math.Clamp( heightValue, 0, 1 );\n\t}\n\n\tpublic static float Plateau(\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tlong seed,\n\t\tfloat minHeight,\n\t\tbool warp,\n\t\tfloat warpSize,\n\t\tfloat warpStrength\n\t\n\t)\n\t{\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\t\tfloat nx = (x / (float)width) * 2 - 1; // Normalize x to range [-1, 1]\n\t\tfloat ny = (y / (float)height) * 2 - 1; // Normalize y to range [-1, 1]\n\n\t\tfloat plateauHeight = 0.8f; // Maximum height of the plateau\n\t\tfloat widthRatio = 0.75f; // Width of the side that does not extend to the edge\n\t\tfloat slopeWidthRatio = 0.02f; // Width of the slope transition\n\t\tfloat slopeNoiseStrength = 0.1f; // Strength of additional noise on slopes\n\t\tfloat baseHeight = 0.1f; // Minimum terrain height\n\t\tfloat noiseStrength = 0f; // Strength of noise for natural variation\n\t\tfloat cutInOutStrength = 2f; // Strength of the cut-ins and jut-outs\n\t\tfloat topNoiseStrength = 0f; // Reduced noise strength for the plateau to\n\n\t\t// Apply domain warping for irregularity\n\t\tif ( warp )\n\t\t{\n\t\t\tfloat warpX = OpenSimplex2S.Noise2( seed + 10, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tfloat warpY = OpenSimplex2S.Noise2( seed + 11, nx * warpSize, ny * warpSize ) * warpStrength;\n\t\t\tnx += warpX;\n\t\t\tny += warpY;\n\t\t}\n\n\t\t// Define the plateau boundaries\n\t\tfloat plateauStartX = -1f; // Left edge\n\t\tfloat plateauEndX = widthRatio * 2 - 1; // End of the one side\n\t\tfloat plateauStartY = -1f; // Bottom edge\n\t\tfloat plateauEndY = 1f; // Top edge (extends to the edge)\n\n\t\t// Check if the current point is within the flat plateau region\n\t\tbool isFlatPlateau = nx >= plateauStartX && nx <= plateauEndX && ny >= plateauStartY && ny <= plateauEndY;\n\n\t\t// Flat plateau height\n\t\tfloat heightValue = isFlatPlateau ? plateauHeight : 0f;\n\n\t\t// Add slopes with jut-outs and cut-ins for smooth, natural drop-offs\n\t\tif ( !isFlatPlateau )\n\t\t{\n\t\t\t// Add noise-based cut-ins and jut-outs\n\t\t\tfloat noiseCut = OpenSimplex2S.Noise2( seed + 20, nx * 10.0f, ny * 10.0f ) * cutInOutStrength;\n\t\t\t// Generate additional noise for slopes\n\t\t\tfloat slopeNoise = OpenSimplex2S.Noise2( seed + 30, nx * 15.0f, ny * 15.0f ) * slopeNoiseStrength;\n\n\t\t\t// Left slope\n\t\t\tif ( nx < plateauStartX )\n\t\t\t{\n\t\t\t\tfloat slope = Math.Clamp( 1f - MathF.Abs( (nx - plateauStartX) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );\n\t\t\t\theightValue = Math.Max( heightValue, slope * plateauHeight );\n\t\t\t}\n\t\t\t// Right slope (for the single side ending early)\n\t\t\telse if ( nx > plateauEndX )\n\t\t\t{\n\t\t\t\tfloat slope = Math.Clamp( 1f - MathF.Abs( (nx - plateauEndX) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );\n\t\t\t\theightValue = Math.Max( heightValue, slope * plateauHeight );\n\t\t\t}\n\t\t\t// Bottom slope\n\t\t\tif ( ny < plateauStartY )\n\t\t\t{\n\t\t\t\tfloat slope = Math.Clamp( 1f - MathF.Abs( (ny - plateauStartY) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );\n\t\t\t\theightValue = Math.Max( heightValue, slope * plateauHeight );\n\t\t\t}\n\t\t\t// Top slope\n\t\t\tif ( ny > plateauEndY )\n\t\t\t{\n\t\t\t\tfloat slope = Math.Clamp( 1f - MathF.Abs( (ny - plateauEndY) / slopeWidthRatio ) + noiseCut + slopeNoise, 0f, 1f );\n\t\t\t\theightValue = Math.Max( heightValue, slope * plateauHeight );\n\t\t\t}\n\t\t}\n\n\t\t// Add noise for terrain variation\n\t\tfloat baseNoise = OpenSimplex2S.Noise2( seed + 100, nx * 8.0f, ny * 8.0f ) * topNoiseStrength;\n\n\t\tfloat fineNoise = OpenSimplex2S.Noise2( seed + 1, nx * 16.0f, ny * 16.0f ) * (noiseStrength / 2);\n\n\t\t// Add the noise and base height to the terrain\n\t\theightValue += baseNoise + fineNoise + baseHeight;\n\n\t\t// Ensure the base terrain height does not fall below baseHeight\n\t\theightValue = Math.Max( heightValue, baseHeight );\n\n\t\tvar heightValueBase = Math.Max( heightValue, minHeight );\n\n\t\t// Clamp the final height\n\t\treturn Math.Clamp( heightValueBase, 0, 1 );\n\t}\n\n\tprivate static float SmoothStep( float edge0, float edge1, float x )\n\t{\n\t\tx = Math.Clamp( (x - edge0) / (edge1 - edge0), 0.0f, 1.0f ); // Normalize to [0, 1]\n\t\treturn x * x * (3 - 2 * x); // Smoothstep formula\n\t}\n\n}\n"
},
{
"Ident": "sturnus.terraingenerationtool",
"Path": "Editor/Features/RiverStream.cs",
"FileName": "RiverStream.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 339832,
"Code": "using Editor;\nusing Sandbox;\nusing System;\n\nnamespace Sturnus.TerrainGenerationTool.RiverStream;\npublic static class RiverStream\n{\n\tpublic static float[,] AddRiversAndStreams(\n\tfloat[,] heightmap,\n\tint frequency, // Number of rivers/streams\n\tfloat widthScale, // Relative width of rivers/streams\n\tlong seed\n)\n\t{\n\t\tint width = heightmap.GetLength( 0 );\n\t\tint height = heightmap.GetLength( 1 );\n\t\tfloat[,] modifiedHeightmap = (float[,])heightmap.Clone();\n\t\tRandom random = new Random( (int)(seed & 0xFFFFFFFF) );\n\n\t\t// Generate river starting points based on frequency\n\t\tfor ( int i = 0; i < frequency; i++ )\n\t\t{\n\t\t\tint startX = random.Next( 0, width );\n\t\t\tint startY = random.Next( 0, height );\n\n\t\t\t// Ensure the river starts at a relatively high elevation\n\t\t\twhile ( modifiedHeightmap[startX, startY] < 0.5f )\n\t\t\t{\n\t\t\t\tstartX = random.Next( 0, width );\n\t\t\t\tstartY = random.Next( 0, height );\n\t\t\t}\n\n\t\t\t// Trace the river path\n\t\t\tAddRiverPath( modifiedHeightmap, startX, startY, width, height, widthScale, random );\n\t\t}\n\n\t\treturn modifiedHeightmap;\n\t}\n\n\tprivate static void AddRiverPath(\n\t\tfloat[,] heightmap,\n\t\tint startX,\n\t\tint startY,\n\t\tint width,\n\t\tint height,\n\t\tfloat widthScale,\n\t\tRandom random\n\t)\n\t{\n\t\tint currentX = startX;\n\t\tint currentY = startY;\n\n\t\t// Determine the river width based on widthScale\n\t\tint riverWidth = Math.Max( 1, (int)(widthScale * width) );\n\n\t\tfor ( int steps = 0; steps < width * 2; steps++ ) // Ensure rivers stretch long distances\n\t\t{\n\t\t\t// Lower the terrain at the current position to form a river bed\n\t\t\tCarveRiverAtPosition( heightmap, currentX, currentY, riverWidth, width, height );\n\n\t\t\t// Find the next position by prioritizing downhill movement\n\t\t\t(int nextX, int nextY) = FindNextRiverPosition( heightmap, currentX, currentY, width, height, random );\n\n\t\t\t// Stop if the river can no longer flow\n\t\t\tif ( nextX == currentX && nextY == currentY )\n\t\t\t\tbreak;\n\n\t\t\tcurrentX = nextX;\n\t\t\tcurrentY = nextY;\n\t\t}\n\t}\n\n\tprivate static (int, int) FindNextRiverPosition(\n\t\tfloat[,] heightmap,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tRandom random\n\t)\n\t{\n\t\tfloat currentHeight = heightmap[x, y];\n\t\tint nextX = x;\n\t\tint nextY = y;\n\t\tfloat lowestHeight = currentHeight;\n\n\t\t// Check all 8 neighbors to find the steepest downhill path\n\t\tfor ( int offsetY = -1; offsetY <= 1; offsetY++ )\n\t\t{\n\t\t\tfor ( int offsetX = -1; offsetX <= 1; offsetX++ )\n\t\t\t{\n\t\t\t\tint nx = x + offsetX;\n\t\t\t\tint ny = y + offsetY;\n\n\t\t\t\t// Skip out-of-bounds and current position\n\t\t\t\tif ( nx < 0 || nx >= width || ny < 0 || ny >= height || (nx == x && ny == y) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tfloat neighborHeight = heightmap[nx, ny];\n\t\t\t\tif ( neighborHeight < lowestHeight )\n\t\t\t\t{\n\t\t\t\t\tlowestHeight = neighborHeight;\n\t\t\t\t\tnextX = nx;\n\t\t\t\t\tnextY = ny;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add slight randomness to avoid perfectly straight rivers\n\t\tif ( random.NextDouble() < 0.3 ) // 30% chance to adjust path\n\t\t{\n\t\t\tnextX = Math.Clamp( nextX + random.Next( -1, 2 ), 0, width - 1 );\n\t\t\tnextY = Math.Clamp( nextY + random.Next( -1, 2 ), 0, height - 1 );\n\t\t}\n\n\t\treturn (nextX, nextY);\n\t}\n\n\tprivate static void CarveRiverAtPosition(\n\t\tfloat[,] heightmap,\n\t\tint x,\n\t\tint y,\n\t\tint riverWidth,\n\t\tint width,\n\t\tint height\n\t)\n\t{\n\t\tfor ( int offsetY = -riverWidth / 2; offsetY <= riverWidth / 2; offsetY++ )\n\t\t{\n\t\t\tfor ( int offsetX = -riverWidth / 2; offsetX <= riverWidth / 2; offsetX++ )\n\t\t\t{\n\t\t\t\tint nx = x + offsetX;\n\t\t\t\tint ny = y + offsetY;\n\n\t\t\t\t// Ensure we're within bounds\n\t\t\t\tif ( nx >= 0 && nx < width && ny >= 0 && ny < height )\n\t\t\t\t{\n\t\t\t\t\t// Lower the terrain for the river bed\n\t\t\t\t\tfloat distance = MathF.Sqrt( offsetX * offsetX + offsetY * offsetY );\n\t\t\t\t\tfloat factor = Math.Clamp( 1.0f - (distance / (riverWidth / 2.0f)), 0.0f, 1.0f );\n\t\t\t\t\theightmap[nx, ny] -= factor * 0.03f; // Adjust depth for river carving\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\n\n\n}\n"
}
]
}