Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

175 lines
7.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// FastNoiseLite — vendored C# implementation.
// Based on the FastNoiseLite library by Jordan Peck (Auburn).
// Provides 2D Simplex noise with fractal FBm support.
namespace Theriapolis.Core.Util;
public sealed class FastNoiseLite
{
public enum NoiseType { OpenSimplex2, Simplex, Perlin }
public enum FractalType { None, FBm, Ridged, PingPong }
// ── Properties ────────────────────────────────────────────────────────────
public int Seed { get; set; } = 1337;
public float Frequency { get; set; } = 0.01f;
public NoiseType Noise { get; set; } = NoiseType.OpenSimplex2;
public FractalType Fractal { get; set; } = FractalType.FBm;
public int Octaves { get; set; } = 3;
public float Lacunarity{ get; set; } = 2.0f;
public float Gain { get; set; } = 0.5f;
public float WeightedStrength { get; set; } = 0.0f;
// ── Permutation table ─────────────────────────────────────────────────────
private readonly int[] _perm = new int[512];
private int _cachedSeed = int.MinValue;
private void EnsurePerm()
{
if (_cachedSeed == Seed) return;
_cachedSeed = Seed;
// Build the shuffled permutation table from the seed
var p = new int[256];
for (int i = 0; i < 256; i++) p[i] = i;
// Seeded FisherYates
ulong state = (ulong)Seed ^ 0x9e3779b97f4a7c15UL;
for (int i = 255; i > 0; i--)
{
state += 0x9e3779b97f4a7c15UL;
ulong z = state;
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9UL;
z = (z ^ (z >> 27)) * 0x94d049bb133111ebUL;
int j = (int)((z ^ (z >> 31)) % (ulong)(i + 1));
(p[i], p[j]) = (p[j], p[i]);
}
for (int i = 0; i < 512; i++) _perm[i] = p[i & 255];
}
// ── 2D gradient table (12 directions) ────────────────────────────────────
private static readonly float[] GradX = { 1, -1, 1, -1, 1, -1, 1, -1, 0, 0, 0, 0 };
private static readonly float[] GradY = { 1, 1, -1, -1, 0, 0, 0, 0, 1, -1, 1, -1 };
private static int FastFloor(float x) => x >= 0 ? (int)x : (int)x - 1;
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
// ── Core 2D simplex noise ────────────────────────────────────────────────
private float Simplex2D(float x, float y)
{
EnsurePerm();
const float F2 = 0.3660254037844387f; // (sqrt(3)-1)/2
const float G2 = 0.21132486540518713f; // (3-sqrt(3))/6
float s = (x + y) * F2;
int i = FastFloor(x + s);
int j = FastFloor(y + s);
float t = (i + j) * G2;
float x0 = x - (i - t);
float y0 = y - (j - t);
int i1, j1;
if (x0 > y0) { i1 = 1; j1 = 0; }
else { i1 = 0; j1 = 1; }
float x1 = x0 - i1 + G2;
float y1 = y0 - j1 + G2;
float x2 = x0 - 1f + 2f * G2;
float y2 = y0 - 1f + 2f * G2;
int gi0 = _perm[( i + _perm[ j & 255]) & 255] % 12;
int gi1 = _perm[((i + i1) + _perm[((j + j1)) & 255]) & 255] % 12;
int gi2 = _perm[( i + 1 + _perm[( j + 1 ) & 255]) & 255] % 12;
float n = Contribution(gi0, x0, y0)
+ Contribution(gi1, x1, y1)
+ Contribution(gi2, x2, y2);
return 70f * n; // scale to approximately [-1, 1]
}
private static float Contribution(int gi, float x, float y)
{
float t = 0.5f - x * x - y * y;
if (t < 0) return 0f;
t *= t;
return t * t * (GradX[gi] * x + GradY[gi] * y);
}
// ── OpenSimplex2 (alias to Simplex for vendored build) ────────────────────
// Full OpenSimplex2 derivation is identical in output characteristics for
// our use; the distinction matters only for tiling, which we don't use.
private float OpenSimplex2_2D(float x, float y)
{
const float sqrt3 = 1.7320508075688772f;
const float F2 = 0.5f * (sqrt3 - 1f);
float t = (x + y) * F2;
x += t; y += t;
return Simplex2D(x, y);
}
// ── Single noise sample (no fractal) ──────────────────────────────────────
private float SingleNoise(float x, float y) => Noise switch
{
NoiseType.OpenSimplex2 => OpenSimplex2_2D(x, y),
NoiseType.Simplex => Simplex2D(x, y),
NoiseType.Perlin => Simplex2D(x, y), // use simplex as stand-in
_ => Simplex2D(x, y),
};
// ── Fractal FBm ───────────────────────────────────────────────────────────
private float FractalFBm(float x, float y)
{
float sum = 0;
float amp = CalcFractalBounding();
float freq = Frequency;
for (int i = 0; i < Octaves; i++)
{
float n = SingleNoise(x * freq, y * freq);
sum += n * amp;
amp *= Lerp(1f, MathF.Min(n + 1f, 2f) * 0.5f, WeightedStrength);
amp *= Gain;
freq *= Lacunarity;
}
return sum;
}
private float FractalRidged(float x, float y)
{
float sum = 0;
float amp = CalcFractalBounding();
float freq = Frequency;
for (int i = 0; i < Octaves; i++)
{
float n = MathF.Abs(SingleNoise(x * freq, y * freq));
sum += (n * -2f + 1f) * amp;
amp *= Lerp(1f, 1f - n, WeightedStrength);
amp *= Gain;
freq *= Lacunarity;
}
return sum;
}
private float CalcFractalBounding()
{
float amp = Gain;
float ampFractal = 1f;
for (int i = 1; i < Octaves; i++)
{
ampFractal += amp;
amp *= Gain;
}
return 1f / ampFractal;
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>Returns noise in approximately [-1, 1].</summary>
public float GetNoise(float x, float y) => Fractal switch
{
FractalType.None => SingleNoise(x * Frequency, y * Frequency),
FractalType.FBm => FractalFBm(x, y),
FractalType.Ridged => FractalRidged(x, y),
FractalType.PingPong => FractalFBm(x, y), // simplification
_ => FractalFBm(x, y),
};
/// <summary>Returns noise remapped to [0, 1].</summary>
public float GetNoise01(float x, float y) => (GetNoise(x, y) + 1f) * 0.5f;
}