diff --git a/SharpEngine.Core.Numerics/MathExtensions.cs b/SharpEngine.Core.Numerics/MathExtensions.cs new file mode 100644 index 0000000..21881ae --- /dev/null +++ b/SharpEngine.Core.Numerics/MathExtensions.cs @@ -0,0 +1,64 @@ +namespace SharpEngine.Core.Numerics; + +/// +/// Provides mathematical utility functions. +/// +public static partial class MathExtensions +{ + /// + /// Returns the largest integer less than or equal to the specified float value. + /// + /// The float value. + /// The largest integer less than or equal to the specified float value. + public static int FastFloor(float value) + { + int integer = (int)value; + + return value < integer ? integer - 1 : integer; + } + + /// + /// Smooths the input value. + /// + /// The input value. + /// The smoothed value. + public static float Fade(float t) + { + return t * t * t * (t * (t * 6f - 15f) + 10f); + } + + /// + /// Performs linear interpolation between two values. + /// + /// The start value. + /// The end value. + /// The interpolation factor. + /// The interpolated value. + public static float Lerp(float a, float b, float t) + { + return a + t * (b - a); + } + + /// + /// Computes the gradient for a given hash and coordinates. + /// + /// The hash value. + /// The x coordinate. + /// The y coordinate. + /// The z coordinate. + /// The computed gradient. + public static float Grad(int hash, float x, float y, float z) + { + int h = hash & 15; + + float u = h < 8 ? x : y; + float v = h < 4 + ? y + : h is 12 or 14 + ? x + : z; + + return ((h & 1) == 0 ? u : -u) + + ((h & 2) == 0 ? v : -v); + } +} diff --git a/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs b/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs new file mode 100644 index 0000000..d285dc2 --- /dev/null +++ b/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs @@ -0,0 +1,45 @@ +namespace SharpEngine.Core.Numerics.Noise; + +/// +/// Defines a noise generator with controllable fractal parameters. +/// +public interface INoiseGenerator +{ + /// Gets or sets the scale factor applied to the input coordinates. + /// + /// Higher values make the noise appear more zoomed out. + /// + float Scale { get; set; } + + /// Gets or sets the number of octaves (noise layers) to combine. + int Octaves { get; set; } + + /// + /// Gets or sets the persistence, + /// which controls how much each octave contributes relative to the previous one (amplitude decay per octave). + /// + float Persistence { get; set; } + + /// + /// Gets or sets the lacunarity, + /// which controls how quickly the frequency increases per octave. + /// + float Lacunarity { get; set; } + + /// + /// Samples the noise at the given 2D coordinates. + /// + /// The X coordinate. + /// The Z coordinate. + /// A normalized value in the range [0, 1]. + float Sample2D(float x, float z); + + /// + /// Samples the noise at the given 3D coordinates. + /// + /// The X coordinate. + /// The Y coordinate. + /// The Z coordinate. + /// A normalized value in the range [0, 1]. + float Sample3D(float x, float y, float z); +} diff --git a/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs new file mode 100644 index 0000000..a961cd8 --- /dev/null +++ b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs @@ -0,0 +1,191 @@ +using MathEx = SharpEngine.Core.Numerics.MathExtensions; + +namespace SharpEngine.Core.Numerics.Noise; + +/// +/// Deterministic Perlin noise generator with fractal octave support. +/// +public sealed class PerlinNoiseGenerator : INoiseGenerator +{ + private readonly int[] _permutation = new int[512]; + + private float _scale = 32f; + private int _octaves = 4; + private float _persistence = 0.5f; + private float _lacunarity = 2f; + + /// + /// Creates a new Perlin noise generator. + /// + /// The seed used to generate the permutation table. + public PerlinNoiseGenerator(int seed = 1337) + { + InitializePermutation(seed); + } + + /// + public float Scale + { + get => _scale; + set => _scale = value <= 0f ? 0.0001f : value; + } + + /// + public int Octaves + { + get => _octaves; + set => _octaves = Math.Max(1, value); + } + + /// + public float Persistence + { + get => _persistence; + set => _persistence = Math.Max(0f, value); + } + + /// + public float Lacunarity + { + get => _lacunarity; + set => _lacunarity = Math.Max(0.0001f, value); + } + + /// + public float Sample2D(float x, float z) + { + float total = 0f; + float amplitude = 1f; + float frequency = 1f; + float maxAmplitude = 0f; + + for (int i = 0; i < Octaves; i++) + { + float sampleX = x / Scale * frequency; + float sampleZ = z / Scale * frequency; + + float noise = Perlin(sampleX, 0f, sampleZ); + + total += noise * amplitude; + maxAmplitude += amplitude; + + amplitude *= Persistence; + frequency *= Lacunarity; + } + + float normalized = total / maxAmplitude; + + return ToUnitRange(normalized); + } + + /// + public float Sample3D(float x, float y, float z) + { + float total = 0f; + float amplitude = 1f; + float frequency = 1f; + float maxAmplitude = 0f; + + for (int i = 0; i < Octaves; i++) + { + float sampleX = x / Scale * frequency; + float sampleY = y / Scale * frequency; + float sampleZ = z / Scale * frequency; + + float noise = Perlin(sampleX, sampleY, sampleZ); + + total += noise * amplitude; + maxAmplitude += amplitude; + + amplitude *= Persistence; + frequency *= Lacunarity; + } + + float normalized = total / maxAmplitude; + + return ToUnitRange(normalized); + } + + private void InitializePermutation(int seed) + { + int[] source = new int[256]; + + for (int i = 0; i < source.Length; i++) + source[i] = i; + + Random random = new(seed); + + for (int i = source.Length - 1; i > 0; i--) + { + int swapIndex = random.Next(i + 1); + + (source[i], source[swapIndex]) = (source[swapIndex], source[i]); + } + + for (int i = 0; i < 512; i++) + _permutation[i] = source[i & 255]; + } + + private float Perlin(float x, float y, float z) + { + int xi = MathEx.FastFloor(x) & 255; + int yi = MathEx.FastFloor(y) & 255; + int zi = MathEx.FastFloor(z) & 255; + + float xf = x - MathEx.FastFloor(x); + float yf = y - MathEx.FastFloor(y); + float zf = z - MathEx.FastFloor(z); + + float u = MathEx.Fade(xf); + float v = MathEx.Fade(yf); + float w = MathEx.Fade(zf); + + var yiOffset = _permutation[xi] + yi; + + int aaa = _permutation[_permutation[yiOffset] + zi]; + int aba = _permutation[_permutation[yiOffset + 1] + zi]; + int aab = _permutation[_permutation[yiOffset] + zi + 1]; + int abb = _permutation[_permutation[yiOffset + 1] + zi + 1]; + + var yiOffsetNext = _permutation[xi + 1] + yi; + + int baa = _permutation[_permutation[yiOffsetNext] + zi]; + int bba = _permutation[_permutation[yiOffsetNext + 1] + zi]; + int bab = _permutation[_permutation[yiOffsetNext] + zi + 1]; + int bbb = _permutation[_permutation[yiOffsetNext + 1] + zi + 1]; + + float x1 = MathEx.Lerp( + MathEx.Grad(aaa, xf, yf, zf), + MathEx.Grad(baa, xf - 1f, yf, zf), + u); + + float x2 = MathEx.Lerp( + MathEx.Grad(aba, xf, yf - 1f, zf), + MathEx.Grad(bba, xf - 1f, yf - 1f, zf), + u); + + float y1 = MathEx.Lerp(x1, x2, v); + + x1 = MathEx.Lerp( + MathEx.Grad(aab, xf, yf, zf - 1f), + MathEx.Grad(bab, xf - 1f, yf, zf - 1f), + u); + + x2 = MathEx.Lerp( + MathEx.Grad(abb, xf, yf - 1f, zf - 1f), + MathEx.Grad(bbb, xf - 1f, yf - 1f, zf - 1f), + u); + + float y2 = MathEx.Lerp(x1, x2, v); + + return MathEx.Lerp(y1, y2, w); + } + + private static float ToUnitRange(float value) + { + // Perlin is roughly in [-1, 1], so remap to [0, 1]. + float result = value * 0.5f + 0.5f; + + return Math.Clamp(result, 0f, 1f); + } +} \ No newline at end of file