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:
+ *
+ * - Generate a base grid of random values in [0, 1).
+ * - For each octave k, compute a layer by bilinear interpolation of the base
+ * grid
+ * at period 2^k.
+ * - Blend all layers from coarse to fine using a geometric series of
+ * amplitudes
+ * controlled by {@code persistence}, then normalize to [0, 1].
+ *
+ *
+ *
+ * For background see:
+ * Perlin Noise.
+ *
+ *
+ * Constraints and notes:
+ *
+ * - {@code width} and {@code height} should be positive.
+ * - {@code octaveCount} must be at least 1 (0 would lead to a division by
+ * zero).
+ * - {@code persistence} should be in (0, 1], typical values around
+ * 0.5–0.8.
+ * - Given the same seed and parameters, results are deterministic.
+ *
*/
+
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);
+ }
+}