diff --git a/src/main/java/com/thealgorithms/others/PerlinNoise.java b/src/main/java/com/thealgorithms/others/PerlinNoise.java index e6551ed6b9ee..d97e3395ff18 100644 --- a/src/main/java/com/thealgorithms/others/PerlinNoise.java +++ b/src/main/java/com/thealgorithms/others/PerlinNoise.java @@ -4,99 +4,156 @@ import java.util.Scanner; /** - * For detailed info and implementation see: Perlin-Noise + * Utility for generating 2D value-noise blended across octaves (commonly known + * as Perlin-like noise). + * + *

+ * The implementation follows the classic approach of: + *

    + *
  1. Generate a base grid of random values in [0, 1).
  2. + *
  3. For each octave k, compute a layer by bilinear interpolation of the base + * grid + * at period 2^k.
  4. + *
  5. Blend all layers from coarse to fine using a geometric series of + * amplitudes + * controlled by {@code persistence}, then normalize to [0, 1].
  6. + *
+ * + *

+ * For background see: + * Perlin Noise. + * + *

+ * Constraints and notes: + *

*/ + public final class PerlinNoise { private PerlinNoise() { } /** - * @param width width of noise array - * @param height height of noise array - * @param octaveCount numbers of layers used for blending noise - * @param persistence value of impact each layer get while blending - * @param seed used for randomizer - * @return float array containing calculated "Perlin-Noise" values + * Generate a 2D array of blended noise values normalized to [0, 1]. + * + * @param width width of the noise array (columns) + * @param height height of the noise array (rows) + * @param octaveCount number of octaves (layers) to blend; must be >= 1 + * @param persistence per-octave amplitude multiplier in (0, 1] + * @param seed seed for the random base grid + * @return a {@code width x height} array containing blended noise values in [0, + * 1] */ static float[][] generatePerlinNoise(int width, int height, int octaveCount, float persistence, long seed) { - final float[][] base = new float[width][height]; - final float[][] perlinNoise = new float[width][height]; - final float[][][] noiseLayers = new float[octaveCount][][]; + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("width and height must be > 0"); + } + if (octaveCount < 1) { + throw new IllegalArgumentException("octaveCount must be >= 1"); + } + if (!(persistence > 0f && persistence <= 1f)) { // using > to exclude 0 and NaN + throw new IllegalArgumentException("persistence must be in (0, 1]"); + } + final float[][] base = createBaseGrid(width, height, seed); + final float[][][] layers = createLayers(base, width, height, octaveCount); + return blendAndNormalize(layers, width, height, persistence); + } + + /** Create the base random lattice values in [0,1). */ + static float[][] createBaseGrid(int width, int height, long seed) { + final float[][] base = new float[width][height]; Random random = new Random(seed); - // fill base array with random values as base for noise for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { base[x][y] = random.nextFloat(); } } + return base; + } - // calculate octaves with different roughness + /** Pre-compute each octave layer at increasing frequency. */ + static float[][][] createLayers(float[][] base, int width, int height, int octaveCount) { + final float[][][] noiseLayers = new float[octaveCount][][]; for (int octave = 0; octave < octaveCount; octave++) { noiseLayers[octave] = generatePerlinNoiseLayer(base, width, height, octave); } + return noiseLayers; + } + /** Blend layers using geometric amplitudes and normalize to [0,1]. */ + static float[][] blendAndNormalize(float[][][] layers, int width, int height, float persistence) { + final int octaveCount = layers.length; + final float[][] out = new float[width][height]; float amplitude = 1f; float totalAmplitude = 0f; - // calculate perlin noise by blending each layer together with specific persistence for (int octave = octaveCount - 1; octave >= 0; octave--) { amplitude *= persistence; totalAmplitude += amplitude; - + final float[][] layer = layers[octave]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { - // adding each value of the noise layer to the noise - // by increasing amplitude the rougher noises will have more impact - perlinNoise[x][y] += noiseLayers[octave][x][y] * amplitude; + out[x][y] += layer[x][y] * amplitude; } } } - // normalize values so that they stay between 0..1 + if (totalAmplitude <= 0f || Float.isInfinite(totalAmplitude) || Float.isNaN(totalAmplitude)) { + throw new IllegalStateException("Invalid totalAmplitude computed during normalization"); + } + + final float invTotal = 1f / totalAmplitude; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { - perlinNoise[x][y] /= totalAmplitude; + out[x][y] *= invTotal; } } - - return perlinNoise; + return out; } /** - * @param base base random float array - * @param width width of noise array + * Generate a single octave layer by bilinear interpolation of a base grid at a + * given octave (period = 2^octave). + * + * @param base base random float array of size {@code width x height} + * @param width width of noise array * @param height height of noise array - * @param octave current layer - * @return float array containing calculated "Perlin-Noise-Layer" values + * @param octave current octave (0 for period 1, 1 for period 2, ...) + * @return float array containing the octave's interpolated values */ static float[][] generatePerlinNoiseLayer(float[][] base, int width, int height, int octave) { float[][] perlinNoiseLayer = new float[width][height]; - // calculate period (wavelength) for different shapes + // Calculate period (wavelength) for different shapes. int period = 1 << octave; // 2^k float frequency = 1f / period; // 1/2^k for (int x = 0; x < width; x++) { - // calculates the horizontal sampling indices + // Calculate the horizontal sampling indices. int x0 = (x / period) * period; int x1 = (x0 + period) % width; - float horizintalBlend = (x - x0) * frequency; + float horizontalBlend = (x - x0) * frequency; for (int y = 0; y < height; y++) { - // calculates the vertical sampling indices + // Calculate the vertical sampling indices. int y0 = (y / period) * period; int y1 = (y0 + period) % height; float verticalBlend = (y - y0) * frequency; - // blend top corners - float top = interpolate(base[x0][y0], base[x1][y0], horizintalBlend); + // Blend top corners. + float top = interpolate(base[x0][y0], base[x1][y0], horizontalBlend); - // blend bottom corners - float bottom = interpolate(base[x0][y1], base[x1][y1], horizintalBlend); + // Blend bottom corners. + float bottom = interpolate(base[x0][y1], base[x1][y1], horizontalBlend); - // blend top and bottom interpolation to get the final blend value for this cell + // Blend top and bottom interpolation to get the final value for this cell. perlinNoiseLayer[x][y] = interpolate(top, bottom, verticalBlend); } } @@ -105,16 +162,21 @@ static float[][] generatePerlinNoiseLayer(float[][] base, int width, int height, } /** - * @param a value of point a - * @param b value of point b - * @param alpha determine which value has more impact (closer to 0 -> a, - * closer to 1 -> b) - * @return interpolated value + * Linear interpolation between two values. + * + * @param a value at alpha = 0 + * @param b value at alpha = 1 + * @param alpha interpolation factor in [0, 1] + * @return interpolated value {@code (1 - alpha) * a + alpha * b} */ static float interpolate(float a, float b, float alpha) { return a * (1 - alpha) + alpha * b; } + /** + * Small demo that prints a text representation of the noise using a provided + * character set. + */ public static void main(String[] args) { Scanner in = new Scanner(System.in); @@ -148,7 +210,7 @@ public static void main(String[] args) { final char[] chars = charset.toCharArray(); final int length = chars.length; final float step = 1f / length; - // output based on charset + // Output based on charset thresholds. for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { float value = step; diff --git a/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java new file mode 100644 index 000000000000..88c043ad9aa3 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java @@ -0,0 +1,103 @@ +package com.thealgorithms.others; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PerlinNoiseTest { + + @Test + @DisplayName("generatePerlinNoise returns array with correct dimensions") + void testDimensions() { + int w = 8; + int h = 6; + float[][] noise = PerlinNoise.generatePerlinNoise(w, h, 4, 0.6f, 123L); + assertThat(noise).hasDimensions(w, h); + } + + @Test + @DisplayName("All values are within [0,1] after normalization") + void testRange() { + int w = 16; + int h = 16; + float[][] noise = PerlinNoise.generatePerlinNoise(w, h, 5, 0.7f, 42L); + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + assertThat(noise[x][y]).isBetween(0f, 1f); + } + } + } + + @Test + @DisplayName("Deterministic for same parameters and seed") + void testDeterminism() { + int w = 10; + int h = 10; + long seed = 98765L; + float[][] a = PerlinNoise.generatePerlinNoise(w, h, 3, 0.5f, seed); + float[][] b = PerlinNoise.generatePerlinNoise(w, h, 3, 0.5f, seed); + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + assertThat(a[x][y]).isEqualTo(b[x][y]); + } + } + } + + @Test + @DisplayName("Different seeds produce different outputs (probabilistically)") + void testDifferentSeeds() { + int w = 12; + int h = 12; + float[][] a = PerlinNoise.generatePerlinNoise(w, h, 4, 0.8f, 1L); + float[][] b = PerlinNoise.generatePerlinNoise(w, h, 4, 0.8f, 2L); + + // Count exact equalities; expect very few or none. + int equalCount = 0; + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + if (Float.compare(a[x][y], b[x][y]) == 0) { + equalCount++; + } + } + } + assertThat(equalCount).isLessThan(w * h / 10); // less than 10% equal exact values + } + + @Test + @DisplayName("Interpolation endpoints are respected") + void testInterpolateEndpoints() { + assertThat(PerlinNoise.interpolate(0f, 1f, 0f)).isEqualTo(0f); + assertThat(PerlinNoise.interpolate(0f, 1f, 1f)).isEqualTo(1f); + assertThat(PerlinNoise.interpolate(0.2f, 0.8f, 0.5f)).isEqualTo(0.5f); + } + + @Test + @DisplayName("Single octave reduces to bilinear interpolation of base grid") + void testSingleOctaveLayer() { + int w = 8; + int h = 8; + long seed = 7L; + float[][] base = PerlinNoise.createBaseGrid(w, h, seed); + float[][] layer = PerlinNoise.generatePerlinNoiseLayer(base, w, h, 0); // period=1 + // With period = 1, x0=x, x1=(x+1)%w etc. Values should be smooth and within + // [0,1] + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + assertThat(layer[x][y]).isBetween(0f, 1f); + } + } + } + + @Test + @DisplayName("Invalid inputs are rejected") + void testInvalidInputs() { + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(0, 5, 1, 0.5f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, -1, 1, 0.5f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 0, 0.5f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, 0f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, Float.NaN, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, 1.1f, 1L)).isInstanceOf(IllegalArgumentException.class); + } +}