🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (14 total matches found)
sturnus.terraingenerationtool / Editor/OpenSimplex2S.cs
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;
		}
	}
}
sturnus.terraingenerationtool / Editor/TerrainShapes/Islands.cs
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;
	}

}
sturnus.terraingenerationtool / Editor/TerrainShapes/Planetary.cs
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;
	}


}
sturnus.terraingenerationtool / Editor/TerrainBrushes/ExtendedTerrainTool.cs
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;
	}
}
sturnus.terraingenerationtool / .obj/__compiler_extra.cs
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")]
sturnus.terraingenerationtool / Code/File.cs
Game library
using Sandbox;

sturnus.terraingenerationtool / File.cs
Game library
using Sandbox;

sturnus.terraingenerationtool / Editor/Noise/Lanczos.cs
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;
		}
	}
}
sturnus.terraingenerationtool / Editor/TerrainShapes/Volcanic.cs
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 );
	}

}
sturnus.terraingenerationtool / Editor/TerrainShapes/Mountainous.cs
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 );
	}
}
sturnus.terraingenerationtool / Editor/TerrainShapes/Sea.cs
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 );
	}




}
sturnus.terraingenerationtool / Editor/TerrainGenerationTool.cs
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();
	}
}

sturnus.terraingenerationtool / Editor/TerrainShapes/Realistic.cs
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
	}

}
sturnus.terraingenerationtool / Editor/Features/RiverStream.cs
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"
        }
    ]
}