From de5cbfce707b5b6cb02a7aded0109666e679fedb Mon Sep 17 00:00:00 2001 From: Hardik Pawar Date: Sun, 5 Oct 2025 12:47:45 +0530 Subject: [PATCH 1/3] refactor: Enhance docs, add tests in `PerlinNoise` --- .../com/thealgorithms/others/PerlinNoise.java | 134 ++++++++++++------ .../thealgorithms/others/PerlinNoiseTest.java | 104 ++++++++++++++ 2 files changed, 196 insertions(+), 42 deletions(-) create mode 100644 src/test/java/com/thealgorithms/others/PerlinNoiseTest.java diff --git a/src/main/java/com/thealgorithms/others/PerlinNoise.java b/src/main/java/com/thealgorithms/others/PerlinNoise.java index e6551ed6b9ee..cb926794d66e 100644 --- a/src/main/java/com/thealgorithms/others/PerlinNoise.java +++ b/src/main/java/com/thealgorithms/others/PerlinNoise.java @@ -4,99 +4,144 @@ 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() { - } + 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 + * 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 +150,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 +198,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..bac0bfe2daa7 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java @@ -0,0 +1,104 @@ +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, 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, 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, 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, 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, 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); + } +} From 819b5b4964e3bc8928653ec5d00fb9698e06f755 Mon Sep 17 00:00:00 2001 From: Hardik Pawar Date: Sun, 5 Oct 2025 12:50:42 +0530 Subject: [PATCH 2/3] Fix lint --- .../com/thealgorithms/others/PerlinNoise.java | 54 +++++++++++-------- .../thealgorithms/others/PerlinNoiseTest.java | 18 +++---- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/thealgorithms/others/PerlinNoise.java b/src/main/java/com/thealgorithms/others/PerlinNoise.java index cb926794d66e..d97e3395ff18 100644 --- a/src/main/java/com/thealgorithms/others/PerlinNoise.java +++ b/src/main/java/com/thealgorithms/others/PerlinNoise.java @@ -7,42 +7,54 @@ * Utility for generating 2D value-noise blended across octaves (commonly known * as Perlin-like noise). * - *

The implementation follows the classic approach of: + *

+ * 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. + *
  7. Generate a base grid of random values in [0, 1).
  8. + *
  9. For each octave k, compute a layer by bilinear interpolation of the base + * grid + * at period 2^k.
  10. + *
  11. Blend all layers from coarse to fine using a geometric series of + * amplitudes + * controlled by {@code persistence}, then normalize to [0, 1].
  12. *
* - *

For background see: Perlin Noise. + *

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

Constraints and notes: + *

+ * Constraints and notes: *

*/ + public final class PerlinNoise { - private PerlinNoise() {} + private PerlinNoise() { + } /** * 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 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] + * @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) { if (width <= 0 || height <= 0) { throw new IllegalArgumentException("width and height must be > 0"); } + if (octaveCount < 1) { throw new IllegalArgumentException("octaveCount must be >= 1"); } @@ -110,8 +122,8 @@ static float[][] blendAndNormalize(float[][][] layers, int width, int height, fl * 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 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 octave (0 for period 1, 1 for period 2, ...) * @return float array containing the octave's interpolated values @@ -152,8 +164,8 @@ static float[][] generatePerlinNoiseLayer(float[][] base, int width, int height, /** * Linear interpolation between two values. * - * @param a value at alpha = 0 - * @param b value at alpha = 1 + * @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} */ diff --git a/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java index bac0bfe2daa7..f6b371b149e9 100644 --- a/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java +++ b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java @@ -88,17 +88,11 @@ void testSingleOctaveLayer() { @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); + 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); } } From e61bbd03f103c818f4b1b745453640d9d15e0a76 Mon Sep 17 00:00:00 2001 From: Hardik Pawar Date: Sun, 5 Oct 2025 12:57:02 +0530 Subject: [PATCH 3/3] Fix lint --- .../com/thealgorithms/others/PerlinNoiseTest.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java index f6b371b149e9..88c043ad9aa3 100644 --- a/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java +++ b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java @@ -11,7 +11,8 @@ class PerlinNoiseTest { @Test @DisplayName("generatePerlinNoise returns array with correct dimensions") void testDimensions() { - int w = 8, h = 6; + int w = 8; + int h = 6; float[][] noise = PerlinNoise.generatePerlinNoise(w, h, 4, 0.6f, 123L); assertThat(noise).hasDimensions(w, h); } @@ -19,7 +20,8 @@ void testDimensions() { @Test @DisplayName("All values are within [0,1] after normalization") void testRange() { - int w = 16, h = 16; + 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++) { @@ -31,7 +33,8 @@ void testRange() { @Test @DisplayName("Deterministic for same parameters and seed") void testDeterminism() { - int w = 10, h = 10; + 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); @@ -45,7 +48,8 @@ void testDeterminism() { @Test @DisplayName("Different seeds produce different outputs (probabilistically)") void testDifferentSeeds() { - int w = 12, h = 12; + 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); @@ -72,7 +76,8 @@ void testInterpolateEndpoints() { @Test @DisplayName("Single octave reduces to bilinear interpolation of base grid") void testSingleOctaveLayer() { - int w = 8, h = 8; + 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