Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 103 additions & 41 deletions src/main/java/com/thealgorithms/others/PerlinNoise.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,99 +4,156 @@
import java.util.Scanner;

/**
* For detailed info and implementation see: <a
* href="http://devmag.org.za/2009/04/25/perlin-noise/">Perlin-Noise</a>
* Utility for generating 2D value-noise blended across octaves (commonly known
* as Perlin-like noise).
*
* <p>
* The implementation follows the classic approach of:
* <ol>
* <li>Generate a base grid of random values in [0, 1).</li>
* <li>For each octave k, compute a layer by bilinear interpolation of the base
* grid
* at period 2^k.</li>
* <li>Blend all layers from coarse to fine using a geometric series of
* amplitudes
* controlled by {@code persistence}, then normalize to [0, 1].</li>
* </ol>
*
* <p>
* For background see:
* <a href="http://devmag.org.za/2009/04/25/perlin-noise/">Perlin Noise</a>.
*
* <p>
* Constraints and notes:
* <ul>
* <li>{@code width} and {@code height} should be positive.</li>
* <li>{@code octaveCount} must be at least 1 (0 would lead to a division by
* zero).</li>
* <li>{@code persistence} should be in (0, 1], typical values around
* 0.5–0.8.</li>
* <li>Given the same seed and parameters, results are deterministic.</li>
* </ul>
*/

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);
}
}
Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand Down
103 changes: 103 additions & 0 deletions src/test/java/com/thealgorithms/others/PerlinNoiseTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}