From da1b2d044ee1e63dfc6de991f90d623ce4db8c27 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 12:26:36 +0300 Subject: [PATCH 1/2] #87 Basic perlin noise --- .../Noise/INoiseGenerator.cs | 36 +++ .../Noise/PerlinNoiseGenerator.cs | 226 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs create mode 100644 SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs diff --git a/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs b/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs new file mode 100644 index 0000000..03077d3 --- /dev/null +++ b/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs @@ -0,0 +1,36 @@ +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 = 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..d468d86 --- /dev/null +++ b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs @@ -0,0 +1,226 @@ +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); + } + + /// + /// Gets or sets the scale factor applied to the input coordinates. + /// Higher values make the noise appear more zoomed out. + /// + public float Scale + { + get => _scale; + set => _scale = value <= 0f ? 0.0001f : value; + } + + /// + /// Gets or sets the number of octaves to combine. + /// + public int Octaves + { + get => _octaves; + set => _octaves = Math.Max(1, value); + } + + /// + /// Gets or sets the amplitude multiplier between octaves. + /// + public float Persistence + { + get => _persistence; + set => _persistence = Math.Max(0f, value); + } + + /// + /// Gets or sets the frequency multiplier between octaves. + /// + 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 = FastFloor(x) & 255; + int yi = FastFloor(y) & 255; + int zi = FastFloor(z) & 255; + + float xf = x - FastFloor(x); + float yf = y - FastFloor(y); + float zf = z - FastFloor(z); + + float u = Fade(xf); + float v = Fade(yf); + float w = Fade(zf); + + int aaa = _permutation[_permutation[_permutation[xi] + yi] + zi]; + int aba = _permutation[_permutation[_permutation[xi] + yi + 1] + zi]; + int aab = _permutation[_permutation[_permutation[xi] + yi] + zi + 1]; + int abb = _permutation[_permutation[_permutation[xi] + yi + 1] + zi + 1]; + + int baa = _permutation[_permutation[_permutation[xi + 1] + yi] + zi]; + int bba = _permutation[_permutation[_permutation[xi + 1] + yi + 1] + zi]; + int bab = _permutation[_permutation[_permutation[xi + 1] + yi] + zi + 1]; + int bbb = _permutation[_permutation[_permutation[xi + 1] + yi + 1] + zi + 1]; + + float x1 = Lerp( + Grad(aaa, xf, yf, zf), + Grad(baa, xf - 1f, yf, zf), + u); + + float x2 = Lerp( + Grad(aba, xf, yf - 1f, zf), + Grad(bba, xf - 1f, yf - 1f, zf), + u); + + float y1 = Lerp(x1, x2, v); + + x1 = Lerp( + Grad(aab, xf, yf, zf - 1f), + Grad(bab, xf - 1f, yf, zf - 1f), + u); + + x2 = Lerp( + Grad(abb, xf, yf - 1f, zf - 1f), + Grad(bbb, xf - 1f, yf - 1f, zf - 1f), + u); + + float y2 = Lerp(x1, x2, v); + + return Lerp(y1, y2, w); + } + + private static int FastFloor(float value) + { + int integer = (int)value; + + return value < integer ? integer - 1 : integer; + } + + private static float Fade(float t) + { + return t * t * t * (t * (t * 6f - 15f) + 10f); + } + + private static float Lerp(float a, float b, float t) + { + return a + t * (b - a); + } + + private 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); + } + + 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 From bd5a1d97e161845a6518d53a84f0547b027ab631 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 16:03:03 +0300 Subject: [PATCH 2/2] #87 Documentation and cleanup --- SharpEngine.Core.Numerics/MathExtensions.cs | 64 +++++++++ .../Noise/INoiseGenerator.cs | 15 ++- .../Noise/PerlinNoiseGenerator.cs | 125 +++++++----------- 3 files changed, 121 insertions(+), 83 deletions(-) create mode 100644 SharpEngine.Core.Numerics/MathExtensions.cs 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 index 03077d3..d285dc2 100644 --- a/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs +++ b/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs @@ -5,16 +5,25 @@ namespace SharpEngine.Core.Numerics.Noise; /// public interface INoiseGenerator { - /// Gets or sets the scale factor applied to the input coordinates (higher = zoomed out). + /// 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). + /// + /// 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. + /// + /// Gets or sets the lacunarity, + /// which controls how quickly the frequency increases per octave. + /// float Lacunarity { get; set; } /// diff --git a/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs index d468d86..a961cd8 100644 --- a/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs +++ b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs @@ -1,4 +1,6 @@ -namespace SharpEngine.Core.Numerics.Noise; +using MathEx = SharpEngine.Core.Numerics.MathExtensions; + +namespace SharpEngine.Core.Numerics.Noise; /// /// Deterministic Perlin noise generator with fractal octave support. @@ -21,37 +23,28 @@ public PerlinNoiseGenerator(int seed = 1337) InitializePermutation(seed); } - /// - /// Gets or sets the scale factor applied to the input coordinates. - /// Higher values make the noise appear more zoomed out. - /// + /// public float Scale { get => _scale; set => _scale = value <= 0f ? 0.0001f : value; } - /// - /// Gets or sets the number of octaves to combine. - /// + /// public int Octaves { get => _octaves; set => _octaves = Math.Max(1, value); } - /// - /// Gets or sets the amplitude multiplier between octaves. - /// + /// public float Persistence { get => _persistence; set => _persistence = Math.Max(0f, value); } - /// - /// Gets or sets the frequency multiplier between octaves. - /// + /// public float Lacunarity { get => _lacunarity; @@ -135,85 +128,57 @@ private void InitializePermutation(int seed) private float Perlin(float x, float y, float z) { - int xi = FastFloor(x) & 255; - int yi = FastFloor(y) & 255; - int zi = FastFloor(z) & 255; - - float xf = x - FastFloor(x); - float yf = y - FastFloor(y); - float zf = z - FastFloor(z); - - float u = Fade(xf); - float v = Fade(yf); - float w = Fade(zf); - - int aaa = _permutation[_permutation[_permutation[xi] + yi] + zi]; - int aba = _permutation[_permutation[_permutation[xi] + yi + 1] + zi]; - int aab = _permutation[_permutation[_permutation[xi] + yi] + zi + 1]; - int abb = _permutation[_permutation[_permutation[xi] + yi + 1] + zi + 1]; - - int baa = _permutation[_permutation[_permutation[xi + 1] + yi] + zi]; - int bba = _permutation[_permutation[_permutation[xi + 1] + yi + 1] + zi]; - int bab = _permutation[_permutation[_permutation[xi + 1] + yi] + zi + 1]; - int bbb = _permutation[_permutation[_permutation[xi + 1] + yi + 1] + zi + 1]; - - float x1 = Lerp( - Grad(aaa, xf, yf, zf), - Grad(baa, xf - 1f, yf, zf), - u); + int xi = MathEx.FastFloor(x) & 255; + int yi = MathEx.FastFloor(y) & 255; + int zi = MathEx.FastFloor(z) & 255; - float x2 = Lerp( - Grad(aba, xf, yf - 1f, zf), - Grad(bba, xf - 1f, yf - 1f, zf), - u); + float xf = x - MathEx.FastFloor(x); + float yf = y - MathEx.FastFloor(y); + float zf = z - MathEx.FastFloor(z); - float y1 = Lerp(x1, x2, v); + float u = MathEx.Fade(xf); + float v = MathEx.Fade(yf); + float w = MathEx.Fade(zf); - x1 = Lerp( - Grad(aab, xf, yf, zf - 1f), - Grad(bab, xf - 1f, yf, zf - 1f), - u); + var yiOffset = _permutation[xi] + yi; - x2 = Lerp( - Grad(abb, xf, yf - 1f, zf - 1f), - Grad(bbb, xf - 1f, yf - 1f, zf - 1f), - u); + 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]; - float y2 = Lerp(x1, x2, v); + var yiOffsetNext = _permutation[xi + 1] + yi; - return Lerp(y1, y2, w); - } + 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]; - private static int FastFloor(float value) - { - int integer = (int)value; + float x1 = MathEx.Lerp( + MathEx.Grad(aaa, xf, yf, zf), + MathEx.Grad(baa, xf - 1f, yf, zf), + u); - return value < integer ? integer - 1 : integer; - } + float x2 = MathEx.Lerp( + MathEx.Grad(aba, xf, yf - 1f, zf), + MathEx.Grad(bba, xf - 1f, yf - 1f, zf), + u); - private static float Fade(float t) - { - return t * t * t * (t * (t * 6f - 15f) + 10f); - } + float y1 = MathEx.Lerp(x1, x2, v); - private static float Lerp(float a, float b, float t) - { - return a + t * (b - a); - } + x1 = MathEx.Lerp( + MathEx.Grad(aab, xf, yf, zf - 1f), + MathEx.Grad(bab, xf - 1f, yf, zf - 1f), + u); - private static float Grad(int hash, float x, float y, float z) - { - int h = hash & 15; + x2 = MathEx.Lerp( + MathEx.Grad(abb, xf, yf - 1f, zf - 1f), + MathEx.Grad(bbb, xf - 1f, yf - 1f, zf - 1f), + u); - float u = h < 8 ? x : y; - float v = h < 4 - ? y - : h is 12 or 14 - ? x - : z; + float y2 = MathEx.Lerp(x1, x2, v); - return ((h & 1) == 0 ? u : -u) + - ((h & 2) == 0 ? v : -v); + return MathEx.Lerp(y1, y2, w); } private static float ToUnitRange(float value)