Skip to content

Commit a3ac5e3

Browse files
committed
Add procedural landmass generation demo using Perlin noise
1 parent 88c61d0 commit a3ac5e3

File tree

7 files changed

+699
-0
lines changed

7 files changed

+699
-0
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package engine.demos.landmass;
2+
3+
import math.Color;
4+
import math.Mathf;
5+
6+
/**
7+
* A procedural landmass generator, ported from Sebastian Lague's Unity script. This class generates
8+
* a height map and a color map based on Perlin noise and terrain types. It supports adjustable
9+
* parameters for map size, noise generation, and biome regions.
10+
*
11+
* <p>The generated maps can be used to create terrains with realistic variations in height and
12+
* color.
13+
*/
14+
public class MapGenerator {
15+
16+
private int chunkSize = 241;
17+
private int mapWidth = chunkSize;
18+
private int mapHeight = chunkSize;
19+
private int seed = 221;
20+
private int octaves = 4;
21+
private float scale = 50;
22+
private float persistance = 0.5f;
23+
private float lacunarity = 2f;
24+
private float[][] heightMap;
25+
private TerrainType[] regions;
26+
27+
/** Constructs a new {@code MapGenerator} and initializes the height map and terrain regions. */
28+
public MapGenerator() {
29+
initializeRegions();
30+
heightMap =
31+
Noise.createHeightMap(mapWidth, mapHeight, seed, scale, octaves, persistance, lacunarity);
32+
}
33+
34+
/**
35+
* Initializes the predefined terrain regions for the map. Each region is associated with a
36+
* specific height threshold, name, and color.
37+
*/
38+
private void initializeRegions() {
39+
regions = new TerrainType[8];
40+
41+
regions[0] = new TerrainType(0.3f, "Water Deep", Color.getColorFromInt(52, 99, 196));
42+
regions[1] = new TerrainType(0.4f, "Water Shallow", Color.getColorFromInt(54, 102, 199));
43+
regions[2] = new TerrainType(0.45f, "Sand", Color.getColorFromInt(206, 209, 125));
44+
regions[3] = new TerrainType(0.55f, "Grass", Color.getColorFromInt(86, 151, 24));
45+
regions[4] = new TerrainType(0.6f, "Grass 2", Color.getColorFromInt(62, 107, 18));
46+
regions[5] = new TerrainType(0.7f, "Rock", Color.getColorFromInt(90, 69, 62));
47+
regions[6] = new TerrainType(0.9f, "Rock 2", Color.getColorFromInt(77, 69, 56));
48+
regions[7] = new TerrainType(1.0f, "Snow", Color.getColorFromInt(253, 253, 253));
49+
}
50+
51+
/**
52+
* Generates a grayscale noise map representing the terrain height. The noise values are mapped to
53+
* a color gradient ranging from black (low) to white (high).
54+
*
55+
* @return an array of RGBA color values representing the noise map
56+
*/
57+
public int[] createNoiseMap() {
58+
int[] colorMap = new int[heightMap.length * heightMap[9].length];
59+
60+
for (int y = 0; y < mapHeight; y++) {
61+
for (int x = 0; x < mapWidth; x++) {
62+
float lerpedValue = Mathf.lerp(0, 1, heightMap[x][y]);
63+
colorMap[y * mapWidth + x] = new Color(lerpedValue, lerpedValue, lerpedValue).getRGBA();
64+
}
65+
}
66+
return colorMap;
67+
}
68+
69+
/**
70+
* Generates a color map based on predefined terrain regions. The height values in the height map
71+
* are compared against the thresholds of each terrain type to determine the color.
72+
*
73+
* @return an array of RGBA color values representing the color map
74+
*/
75+
public int[] createColorMap() {
76+
int[] colorMap = new int[mapWidth * mapHeight];
77+
78+
for (int y = 0; y < mapHeight; y++) {
79+
for (int x = 0; x < mapWidth; x++) {
80+
float currentHeight = heightMap[x][y];
81+
for (int i = 0; i < regions.length; i++) {
82+
if (currentHeight <= regions[i].height) {
83+
colorMap[y * mapWidth + x] = regions[i].color.getRGBA();
84+
break;
85+
}
86+
}
87+
}
88+
}
89+
return colorMap;
90+
}
91+
92+
/**
93+
* Returns the generated height map. The height map is a 2D array of floats, where each value
94+
* represents the elevation at a specific point.
95+
*
96+
* @return a 2D float array representing the height map
97+
*/
98+
public float[][] getHeightMap() {
99+
return heightMap;
100+
}
101+
102+
/**
103+
* Represents a terrain type, which includes a height threshold, a name, and a color. Terrain
104+
* types are used to categorize different regions of the generated map.
105+
*/
106+
public class TerrainType {
107+
108+
public float height;
109+
public String name;
110+
public Color color;
111+
112+
/**
113+
* Constructs a new {@code TerrainType} with the specified height threshold, name, and color.
114+
*
115+
* @param height the height threshold for this terrain type
116+
* @param name the name of the terrain type
117+
* @param color the color associated with this terrain type
118+
*/
119+
public TerrainType(float height, String name, Color color) {
120+
this.height = height;
121+
this.name = name;
122+
this.color = color;
123+
}
124+
}
125+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package engine.demos.landmass;
2+
3+
import math.Mathf;
4+
import math.PerlinNoise;
5+
6+
/**
7+
* Utility class for generating procedural height maps using Perlin noise.
8+
*
9+
* <p>This class is a Java adaptation of Sebastian Lague's Unity script for procedural landmass
10+
* creation. It generates a two-dimensional array representing height values for a terrain based on
11+
* various noise parameters.
12+
*/
13+
public class Noise {
14+
15+
/**
16+
* Generates a height map using Perlin noise.
17+
*
18+
* <p>The height map is generated by layering multiple octaves of Perlin noise, each with varying
19+
* amplitude and frequency, allowing for detailed and natural-looking terrain generation.
20+
*
21+
* @param mapWidth the width of the height map to be generated
22+
* @param mapHeight the height of the height map to be generated
23+
* @param seed the seed value for the noise generation, ensuring reproducibility
24+
* @param scale the scale of the noise; smaller values zoom in on noise patterns
25+
* @param octaves the number of noise layers (octaves) to combine for more complex patterns
26+
* @param persistance the rate at which the amplitude of each octave decreases
27+
* @param lacunarity the rate at which the frequency of each octave increases
28+
* @return a two-dimensional float array representing the generated height map
29+
*/
30+
public static float[][] createHeightMap(
31+
int mapWidth,
32+
int mapHeight,
33+
int seed,
34+
float scale,
35+
int octaves,
36+
float persistance,
37+
float lacunarity) {
38+
39+
// Initialize Perlin noise with the provided seed
40+
PerlinNoise perlinNoise = new PerlinNoise(seed);
41+
float[][] noiseMap = new float[mapWidth][mapHeight];
42+
43+
// Prevent division by zero by ensuring a minimum scale value
44+
if (scale <= 0) {
45+
scale = 0.0001f;
46+
}
47+
48+
// Variables to track the min and max noise values for normalization
49+
float maxNoiseHeight = Float.MIN_VALUE;
50+
float minNoiseHeight = Float.MAX_VALUE;
51+
52+
// Generate noise values for each point in the map
53+
for (int y = 0; y < mapHeight; y++) {
54+
for (int x = 0; x < mapWidth; x++) {
55+
56+
float amplitude = 1;
57+
float frequency = 1;
58+
float noiseHeight = 0;
59+
60+
// Generate noise using multiple octaves
61+
for (int i = 0; i < octaves; i++) {
62+
// Sample points in the Perlin noise space
63+
float sampleX = x / scale * frequency;
64+
float sampleY = y / scale * frequency;
65+
66+
// Calculate the Perlin noise value and adjust it to allow negative values
67+
float perlinValue = (float) perlinNoise.noise(sampleX, sampleY) * 2 - 1;
68+
noiseHeight += perlinValue * amplitude;
69+
70+
// Decrease amplitude and increase frequency for the next octave
71+
amplitude *= persistance;
72+
frequency *= lacunarity;
73+
}
74+
75+
// Update min and max noise height values
76+
if (noiseHeight > maxNoiseHeight) {
77+
maxNoiseHeight = noiseHeight;
78+
} else if (noiseHeight < minNoiseHeight) {
79+
minNoiseHeight = noiseHeight;
80+
}
81+
82+
// Assign the calculated noise height to the map
83+
noiseMap[x][y] = noiseHeight;
84+
}
85+
}
86+
87+
// Normalize the noise map values to a range between 0 and 1
88+
for (int y = 0; y < mapHeight; y++) {
89+
for (int x = 0; x < mapWidth; x++) {
90+
noiseMap[x][y] = Mathf.inverseLerp(minNoiseHeight, maxNoiseHeight, noiseMap[x][y]);
91+
}
92+
}
93+
94+
return noiseMap;
95+
}
96+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package engine.demos.landmass;
2+
3+
import engine.components.AbstractComponent;
4+
import engine.components.Geometry;
5+
import engine.components.RenderableComponent;
6+
import engine.render.Material;
7+
import engine.resources.FilterMode;
8+
import engine.resources.Texture2D;
9+
import engine.scene.SceneNode;
10+
import mesh.Mesh3D;
11+
import mesh.creator.primitives.PlaneCreator;
12+
import workspace.ui.Graphics;
13+
14+
/**
15+
* The {@code NoiseMapDisplay} class is responsible for rendering a noise map texture onto a 3D
16+
* plane. It creates a textured plane mesh that displays pixel data generated by procedural noise
17+
* algorithms. This class extends {@link AbstractComponent} and implements {@link
18+
* RenderableComponent}, making it suitable for integration within a 3D scene graph.
19+
*
20+
* <p>Usage:
21+
*
22+
* <ul>
23+
* <li>Create an instance by specifying the width and height of the noise map.
24+
* <li>Set pixel data using the {@link #setPixels(int[])} method.
25+
* <li>The texture is automatically mapped onto a 3D plane and rendered during the scene's render
26+
* phase.
27+
* </ul>
28+
*
29+
* <p>This class is typically used in terrain generation demos to visualize height maps or color
30+
* maps.
31+
*/
32+
public class NoiseMapDisplay extends AbstractComponent implements RenderableComponent {
33+
34+
/** The 3D mesh representing a flat plane on which the noise map texture is rendered. */
35+
private Mesh3D planeMesh;
36+
37+
/** The geometry component that combines the plane mesh with a material for rendering. */
38+
private Geometry planeGeometry;
39+
40+
/** The texture representing the noise map, used to display pixel data on the plane mesh. */
41+
private Texture2D texture;
42+
43+
/**
44+
* Constructs a new {@code NoiseMapDisplay} with a specified texture size.
45+
*
46+
* @param width the width of the noise map texture in pixels
47+
* @param height the height of the noise map texture in pixels
48+
*/
49+
public NoiseMapDisplay(int width, int height) {
50+
createPlaneMesh();
51+
texture = new Texture2D(width, height);
52+
texture.setFilterMode(FilterMode.POINT); // Use point filtering for a pixelated look
53+
}
54+
55+
/**
56+
* Initializes the plane mesh used for displaying the noise map. The plane is created with UV
57+
* coordinates to map the texture correctly.
58+
*/
59+
private void createPlaneMesh() {
60+
planeMesh = new PlaneCreator(30).create();
61+
planeMesh.addUvCoordinate(0, 0);
62+
planeMesh.addUvCoordinate(1, 0);
63+
planeMesh.addUvCoordinate(1, 1);
64+
planeMesh.addUvCoordinate(0, 1);
65+
planeMesh.getFaceAt(0).setUvIndices(0, 1, 2, 3);
66+
}
67+
68+
/**
69+
* Sets the pixel data for the noise map texture. The provided array should match the size of the
70+
* texture (width * height).
71+
*
72+
* @param pixels an array of pixel color values in ARGB format
73+
*/
74+
public void setPixels(int[] pixels) {
75+
texture.setPixels(pixels);
76+
Material material = new Material.Builder().setDiffuseTexture(texture).build();
77+
planeGeometry = new Geometry(planeMesh, material);
78+
}
79+
80+
/**
81+
* Returns the noise map texture used by this display.
82+
*
83+
* @return the {@link Texture2D} instance representing the noise map
84+
*/
85+
public Texture2D getTexture() {
86+
return texture;
87+
}
88+
89+
/**
90+
* Renders the noise map display using the provided {@link Graphics} context. The method checks if
91+
* the plane geometry is initialized before rendering.
92+
*
93+
* @param g the {@link Graphics} context used for rendering
94+
*/
95+
@Override
96+
public void render(Graphics g) {
97+
if (planeGeometry == null) {
98+
return;
99+
}
100+
planeGeometry.render(g);
101+
}
102+
103+
/**
104+
* Called each frame to update the component's state. This implementation does not perform any
105+
* updates, but can be extended if needed.
106+
*
107+
* @param tpf time per frame, in seconds
108+
*/
109+
@Override
110+
public void update(float tpf) {
111+
// No updates are required for this component
112+
}
113+
114+
/**
115+
* Called when this component is attached to a {@link SceneNode}. This method is a lifecycle hook
116+
* that can be used to initialize resources or state.
117+
*/
118+
@Override
119+
public void onAttach() {
120+
// No special actions needed on attach
121+
}
122+
123+
/**
124+
* Called when this component is detached from a {@link SceneNode}. This method is a lifecycle
125+
* hook that can be used to clean up resources or state.
126+
*/
127+
@Override
128+
public void onDetach() {
129+
// No special actions needed on detach
130+
}
131+
}

0 commit comments

Comments
 (0)