Skip to content

Commit de5cbfc

Browse files
committed
refactor: Enhance docs, add tests in PerlinNoise
1 parent a0b6c52 commit de5cbfc

File tree

2 files changed

+196
-42
lines changed

2 files changed

+196
-42
lines changed

src/main/java/com/thealgorithms/others/PerlinNoise.java

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,99 +4,144 @@
44
import java.util.Scanner;
55

66
/**
7-
* For detailed info and implementation see: <a
8-
* href="http://devmag.org.za/2009/04/25/perlin-noise/">Perlin-Noise</a>
7+
* Utility for generating 2D value-noise blended across octaves (commonly known
8+
* as Perlin-like noise).
9+
*
10+
* <p>The implementation follows the classic approach of:
11+
* <ol>
12+
* <li>Generate a base grid of random values in [0, 1).</li>
13+
* <li>For each octave k, compute a layer by bilinear interpolation of the base grid
14+
* at period 2^k.</li>
15+
* <li>Blend all layers from coarse to fine using a geometric series of amplitudes
16+
* controlled by {@code persistence}, then normalize to [0, 1].</li>
17+
* </ol>
18+
*
19+
* <p>For background see: <a href="http://devmag.org.za/2009/04/25/perlin-noise/">Perlin Noise</a>.
20+
*
21+
* <p>Constraints and notes:
22+
* <ul>
23+
* <li>{@code width} and {@code height} should be positive.</li>
24+
* <li>{@code octaveCount} must be at least 1 (0 would lead to a division by zero).</li>
25+
* <li>{@code persistence} should be in (0, 1], typical values around 0.5–0.8.</li>
26+
* <li>Given the same seed and parameters, results are deterministic.</li>
27+
* </ul>
928
*/
1029
public final class PerlinNoise {
11-
private PerlinNoise() {
12-
}
30+
private PerlinNoise() {}
1331

1432
/**
15-
* @param width width of noise array
16-
* @param height height of noise array
17-
* @param octaveCount numbers of layers used for blending noise
18-
* @param persistence value of impact each layer get while blending
19-
* @param seed used for randomizer
20-
* @return float array containing calculated "Perlin-Noise" values
33+
* Generate a 2D array of blended noise values normalized to [0, 1].
34+
*
35+
* @param width width of the noise array (columns)
36+
* @param height height of the noise array (rows)
37+
* @param octaveCount number of octaves (layers) to blend; must be >= 1
38+
* @param persistence per-octave amplitude multiplier in (0, 1]
39+
* @param seed seed for the random base grid
40+
* @return a {@code width x height} array containing blended noise values in [0, 1]
2141
*/
2242
static float[][] generatePerlinNoise(int width, int height, int octaveCount, float persistence, long seed) {
23-
final float[][] base = new float[width][height];
24-
final float[][] perlinNoise = new float[width][height];
25-
final float[][][] noiseLayers = new float[octaveCount][][];
43+
if (width <= 0 || height <= 0) {
44+
throw new IllegalArgumentException("width and height must be > 0");
45+
}
46+
if (octaveCount < 1) {
47+
throw new IllegalArgumentException("octaveCount must be >= 1");
48+
}
49+
if (!(persistence > 0f && persistence <= 1f)) { // using > to exclude 0 and NaN
50+
throw new IllegalArgumentException("persistence must be in (0, 1]");
51+
}
52+
final float[][] base = createBaseGrid(width, height, seed);
53+
final float[][][] layers = createLayers(base, width, height, octaveCount);
54+
return blendAndNormalize(layers, width, height, persistence);
55+
}
2656

57+
/** Create the base random lattice values in [0,1). */
58+
static float[][] createBaseGrid(int width, int height, long seed) {
59+
final float[][] base = new float[width][height];
2760
Random random = new Random(seed);
28-
// fill base array with random values as base for noise
2961
for (int x = 0; x < width; x++) {
3062
for (int y = 0; y < height; y++) {
3163
base[x][y] = random.nextFloat();
3264
}
3365
}
66+
return base;
67+
}
3468

35-
// calculate octaves with different roughness
69+
/** Pre-compute each octave layer at increasing frequency. */
70+
static float[][][] createLayers(float[][] base, int width, int height, int octaveCount) {
71+
final float[][][] noiseLayers = new float[octaveCount][][];
3672
for (int octave = 0; octave < octaveCount; octave++) {
3773
noiseLayers[octave] = generatePerlinNoiseLayer(base, width, height, octave);
3874
}
75+
return noiseLayers;
76+
}
3977

78+
/** Blend layers using geometric amplitudes and normalize to [0,1]. */
79+
static float[][] blendAndNormalize(float[][][] layers, int width, int height, float persistence) {
80+
final int octaveCount = layers.length;
81+
final float[][] out = new float[width][height];
4082
float amplitude = 1f;
4183
float totalAmplitude = 0f;
4284

43-
// calculate perlin noise by blending each layer together with specific persistence
4485
for (int octave = octaveCount - 1; octave >= 0; octave--) {
4586
amplitude *= persistence;
4687
totalAmplitude += amplitude;
47-
88+
final float[][] layer = layers[octave];
4889
for (int x = 0; x < width; x++) {
4990
for (int y = 0; y < height; y++) {
50-
// adding each value of the noise layer to the noise
51-
// by increasing amplitude the rougher noises will have more impact
52-
perlinNoise[x][y] += noiseLayers[octave][x][y] * amplitude;
91+
out[x][y] += layer[x][y] * amplitude;
5392
}
5493
}
5594
}
5695

57-
// normalize values so that they stay between 0..1
96+
if (totalAmplitude <= 0f || Float.isInfinite(totalAmplitude) || Float.isNaN(totalAmplitude)) {
97+
throw new IllegalStateException("Invalid totalAmplitude computed during normalization");
98+
}
99+
100+
final float invTotal = 1f / totalAmplitude;
58101
for (int x = 0; x < width; x++) {
59102
for (int y = 0; y < height; y++) {
60-
perlinNoise[x][y] /= totalAmplitude;
103+
out[x][y] *= invTotal;
61104
}
62105
}
63-
64-
return perlinNoise;
106+
return out;
65107
}
66108

67109
/**
68-
* @param base base random float array
110+
* Generate a single octave layer by bilinear interpolation of a base grid at a
111+
* given octave (period = 2^octave).
112+
*
113+
* @param base base random float array of size {@code width x height}
69114
* @param width width of noise array
70115
* @param height height of noise array
71-
* @param octave current layer
72-
* @return float array containing calculated "Perlin-Noise-Layer" values
116+
* @param octave current octave (0 for period 1, 1 for period 2, ...)
117+
* @return float array containing the octave's interpolated values
73118
*/
74119
static float[][] generatePerlinNoiseLayer(float[][] base, int width, int height, int octave) {
75120
float[][] perlinNoiseLayer = new float[width][height];
76121

77-
// calculate period (wavelength) for different shapes
122+
// Calculate period (wavelength) for different shapes.
78123
int period = 1 << octave; // 2^k
79124
float frequency = 1f / period; // 1/2^k
80125

81126
for (int x = 0; x < width; x++) {
82-
// calculates the horizontal sampling indices
127+
// Calculate the horizontal sampling indices.
83128
int x0 = (x / period) * period;
84129
int x1 = (x0 + period) % width;
85-
float horizintalBlend = (x - x0) * frequency;
130+
float horizontalBlend = (x - x0) * frequency;
86131

87132
for (int y = 0; y < height; y++) {
88-
// calculates the vertical sampling indices
133+
// Calculate the vertical sampling indices.
89134
int y0 = (y / period) * period;
90135
int y1 = (y0 + period) % height;
91136
float verticalBlend = (y - y0) * frequency;
92137

93-
// blend top corners
94-
float top = interpolate(base[x0][y0], base[x1][y0], horizintalBlend);
138+
// Blend top corners.
139+
float top = interpolate(base[x0][y0], base[x1][y0], horizontalBlend);
95140

96-
// blend bottom corners
97-
float bottom = interpolate(base[x0][y1], base[x1][y1], horizintalBlend);
141+
// Blend bottom corners.
142+
float bottom = interpolate(base[x0][y1], base[x1][y1], horizontalBlend);
98143

99-
// blend top and bottom interpolation to get the final blend value for this cell
144+
// Blend top and bottom interpolation to get the final value for this cell.
100145
perlinNoiseLayer[x][y] = interpolate(top, bottom, verticalBlend);
101146
}
102147
}
@@ -105,16 +150,21 @@ static float[][] generatePerlinNoiseLayer(float[][] base, int width, int height,
105150
}
106151

107152
/**
108-
* @param a value of point a
109-
* @param b value of point b
110-
* @param alpha determine which value has more impact (closer to 0 -> a,
111-
* closer to 1 -> b)
112-
* @return interpolated value
153+
* Linear interpolation between two values.
154+
*
155+
* @param a value at alpha = 0
156+
* @param b value at alpha = 1
157+
* @param alpha interpolation factor in [0, 1]
158+
* @return interpolated value {@code (1 - alpha) * a + alpha * b}
113159
*/
114160
static float interpolate(float a, float b, float alpha) {
115161
return a * (1 - alpha) + alpha * b;
116162
}
117163

164+
/**
165+
* Small demo that prints a text representation of the noise using a provided
166+
* character set.
167+
*/
118168
public static void main(String[] args) {
119169
Scanner in = new Scanner(System.in);
120170

@@ -148,7 +198,7 @@ public static void main(String[] args) {
148198
final char[] chars = charset.toCharArray();
149199
final int length = chars.length;
150200
final float step = 1f / length;
151-
// output based on charset
201+
// Output based on charset thresholds.
152202
for (int x = 0; x < width; x++) {
153203
for (int y = 0; y < height; y++) {
154204
float value = step;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.thealgorithms.others;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
9+
class PerlinNoiseTest {
10+
11+
@Test
12+
@DisplayName("generatePerlinNoise returns array with correct dimensions")
13+
void testDimensions() {
14+
int w = 8, h = 6;
15+
float[][] noise = PerlinNoise.generatePerlinNoise(w, h, 4, 0.6f, 123L);
16+
assertThat(noise).hasDimensions(w, h);
17+
}
18+
19+
@Test
20+
@DisplayName("All values are within [0,1] after normalization")
21+
void testRange() {
22+
int w = 16, h = 16;
23+
float[][] noise = PerlinNoise.generatePerlinNoise(w, h, 5, 0.7f, 42L);
24+
for (int x = 0; x < w; x++) {
25+
for (int y = 0; y < h; y++) {
26+
assertThat(noise[x][y]).isBetween(0f, 1f);
27+
}
28+
}
29+
}
30+
31+
@Test
32+
@DisplayName("Deterministic for same parameters and seed")
33+
void testDeterminism() {
34+
int w = 10, h = 10;
35+
long seed = 98765L;
36+
float[][] a = PerlinNoise.generatePerlinNoise(w, h, 3, 0.5f, seed);
37+
float[][] b = PerlinNoise.generatePerlinNoise(w, h, 3, 0.5f, seed);
38+
for (int x = 0; x < w; x++) {
39+
for (int y = 0; y < h; y++) {
40+
assertThat(a[x][y]).isEqualTo(b[x][y]);
41+
}
42+
}
43+
}
44+
45+
@Test
46+
@DisplayName("Different seeds produce different outputs (probabilistically)")
47+
void testDifferentSeeds() {
48+
int w = 12, h = 12;
49+
float[][] a = PerlinNoise.generatePerlinNoise(w, h, 4, 0.8f, 1L);
50+
float[][] b = PerlinNoise.generatePerlinNoise(w, h, 4, 0.8f, 2L);
51+
52+
// Count exact equalities; expect very few or none.
53+
int equalCount = 0;
54+
for (int x = 0; x < w; x++) {
55+
for (int y = 0; y < h; y++) {
56+
if (Float.compare(a[x][y], b[x][y]) == 0) {
57+
equalCount++;
58+
}
59+
}
60+
}
61+
assertThat(equalCount).isLessThan(w * h / 10); // less than 10% equal exact values
62+
}
63+
64+
@Test
65+
@DisplayName("Interpolation endpoints are respected")
66+
void testInterpolateEndpoints() {
67+
assertThat(PerlinNoise.interpolate(0f, 1f, 0f)).isEqualTo(0f);
68+
assertThat(PerlinNoise.interpolate(0f, 1f, 1f)).isEqualTo(1f);
69+
assertThat(PerlinNoise.interpolate(0.2f, 0.8f, 0.5f)).isEqualTo(0.5f);
70+
}
71+
72+
@Test
73+
@DisplayName("Single octave reduces to bilinear interpolation of base grid")
74+
void testSingleOctaveLayer() {
75+
int w = 8, h = 8;
76+
long seed = 7L;
77+
float[][] base = PerlinNoise.createBaseGrid(w, h, seed);
78+
float[][] layer = PerlinNoise.generatePerlinNoiseLayer(base, w, h, 0); // period=1
79+
// With period = 1, x0=x, x1=(x+1)%w etc. Values should be smooth and within
80+
// [0,1]
81+
for (int x = 0; x < w; x++) {
82+
for (int y = 0; y < h; y++) {
83+
assertThat(layer[x][y]).isBetween(0f, 1f);
84+
}
85+
}
86+
}
87+
88+
@Test
89+
@DisplayName("Invalid inputs are rejected")
90+
void testInvalidInputs() {
91+
assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(0, 5, 1, 0.5f, 1L))
92+
.isInstanceOf(IllegalArgumentException.class);
93+
assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, -1, 1, 0.5f, 1L))
94+
.isInstanceOf(IllegalArgumentException.class);
95+
assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 0, 0.5f, 1L))
96+
.isInstanceOf(IllegalArgumentException.class);
97+
assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, 0f, 1L))
98+
.isInstanceOf(IllegalArgumentException.class);
99+
assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, Float.NaN, 1L))
100+
.isInstanceOf(IllegalArgumentException.class);
101+
assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, 1.1f, 1L))
102+
.isInstanceOf(IllegalArgumentException.class);
103+
}
104+
}

0 commit comments

Comments
 (0)