mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
refactor: Enhance docs, add tests in PerlinNoise (#6635)
* refactor: Enhance docs, add tests in `PerlinNoise` * Fix lint * Fix lint --------- Co-authored-by: Deniz Altunkapan <deniz.altunkapan@outlook.com>
This commit is contained in:
@@ -4,99 +4,156 @@ import java.util.Random;
|
||||
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
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
perlinNoise[x][y] /= totalAmplitude;
|
||||
}
|
||||
if (totalAmplitude <= 0f || Float.isInfinite(totalAmplitude) || Float.isNaN(totalAmplitude)) {
|
||||
throw new IllegalStateException("Invalid totalAmplitude computed during normalization");
|
||||
}
|
||||
|
||||
return perlinNoise;
|
||||
final float invTotal = 1f / totalAmplitude;
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
out[x][y] *= invTotal;
|
||||
}
|
||||
}
|
||||
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 @@ public final class PerlinNoise {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 final class PerlinNoise {
|
||||
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;
|
||||
|
||||
103
src/test/java/com/thealgorithms/others/PerlinNoiseTest.java
Normal file
103
src/test/java/com/thealgorithms/others/PerlinNoiseTest.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user