I want to start a new Java 21 project using LWJGL to build a Minecraft-like voxel engine with a Distant Horizons look, but this time with a correct, production-grade architecture from day one.
The world must be infinite in coordinates, streamed, and deterministic. No part of the system should rely on a single global “world window”.
The engine must be designed as a data-pipeline + streaming system where:
- All generation is pure and deterministic
- All data is generated per region (paged) and cached
- The renderer can request any chunk at any time
- The core must always be able to answer without throwing bounds exceptions
- Java 21
- LWJGL renderer (OpenGL)
- Minecraft-style texture atlas
- Infinite world coordinates
- Region-based streaming (like Minecraft / Distant Horizons)
- Deterministic generation (seed + coordinates only)
- Far-field LOD friendly
- UE5-friendly (world data must be serializable, streamable, and engine-agnostic)
- NO OpenGL / LWJGL code outside the renderer module
- Renderer must never know about generation windows, rects, or caching
- Core must never throw “out of bounds” during normal gameplay
app/
- Main entry point
- Wires core ↔ renderer
core/
- All world generation
- Streaming & caching
- Chunk composition
- Zero rendering code
renderer/
- LWJGL implementation
- Later replaceable by UE5
The world is infinite, but data is finite per region.
- World is divided into regions
- Each region contains N×N chunks (e.g. 16×16)
- Each region owns all derived generation layers
- Regions are generated on demand and cached
- Renderer never sees region boundaries
The renderer only ever does:
Chunk chunk = chunkProvider.getChunk(cx, cz);The core is responsible for:
- Determining which region contains the chunk
- Generating or loading the region layers
- Composing the chunk from those layers
Region coordinate
record RegionPos(int rx, int rz) {}Region size :
- Constant: REGION_SIZE_CHUNKS
- Region size in blocks = REGION_SIZE_CHUNKS * CHUNK_SIZE
A region is the streaming and caching unit.
Derived layers (heightmap, biome map, carve mask, surface rules…) are backed by finite arrays.
Each such layer must internally know:
- Its world-space origin
- Its size
- How to safely map (wx, wz) → array index
For this purpose, the core uses an internal helper:
final class LayerRect {
int minX, minZ;
int sizeX, sizeZ;
}Rules for LayerRect:
- It is NOT a world concept
- It is NOT visible to renderer or app
- It is used only internally by array-backed layers
- It exists to enforce correctness and fail fast during development
- Regions own layers; layers internally own a LayerRect.
Each generation phase produces derived data, never blocks.
Heightmap heightmap = terrainGenerator.generateHeightmap(seed, rect);
BiomeMap biomeMap = biomeGenerator.generateBiomes(seed, heightmap);
CarveMask carveMask = carver.generateCarveMask(seed, heightmap);
SurfaceRules surfaceRules = surfaceDecorator.generateSurfaceRules(heightmap, biomeMap);
WaterLayer waterLayer = waterGenerator.generateWaterLayer(seed, heightmap, carveMask);
StructureMap structures = structureBuilder.placeStructures(seed, heightmap, biomeMap);These are grouped into:
record RegionLayers(
Heightmap heightmap,
BiomeMap biomeMap,
CarveMask carveMask,
SurfaceRules surfaceRules,
WaterLayer waterLayer,
StructureMap structureMap
) {}-
interface WorldGenerator { Heightmap generateHeightmap(long seed, LayerRect rect); }
Implementation:
- SimpleWorldGenerator
- Uses FastNoiseLite
- Responsible only for : base terrain shape, continents, oceans, Large-scale elevation, Mountains / hills ...etc
- No biomes, no surface blocks, no structures
- SimpleWorldGenerator
-
interface BiomeGenerator { BiomeMap generateBiomes(long seed, Heightmap heightmap); }
Responsibilities:
- Decide biome per column
- Derived from height + noise
- No block logic
-
interface WorldCarver { CarveMask generateCarveMask(long seed, Heightmap heightmap); }
Implementation:
- DefaultWorldCarver
- Responsibilities: Caves, Ravines, Underground tunnels, Large voids
- No block placement
- DefaultWorldCarver
-
interface SurfaceDecorator { SurfaceRules generateSurfaceRules(Heightmap heightmap, BiomeMap biomeMap); }
Implementation:
- BiomeDecorator
- Responsibilities : Top block, Filler block, Filler depth
- Biome-dependent logic
- No structure placement
- BiomeDecorator
-
interface StructureBuilder { StructureMap placeStructures(long seed, Heightmap heightmap, BiomeMap biomeMap); }
Implementation:
- DefaultStructureBuilder
- Responsibilities : Oak trees, jungle trees, savanna trees, swamp trees, cactus, bushes, flowers ..etc
- Produces structure placement data only
- No block placement
- DefaultStructureBuilder
-
interface WaterGenerator { WaterLayer generateWaterLayer(long seed, Heightmap heightmap, CarveMask carveMask); }
Implementation:
- DefaultWaterGenerator
- Responsibilities: Ocean/sea water, river water levels
- Produces water level per column (or NO_WATER)
- Runs AFTER carving
- Enables future: lakes, swamps, variable river levels
- DefaultWaterGenerator
-
the only place blocks exist
final class ChunkBuilder { Chunk buildChunk(int cx, int cz); short blockAt(int wx, int wy, int wz); }
- Responsibilities :
- Combine: Heightmap, CarveMask, SurfaceRules, WaterLayer, StructureMap
- No generation logic
- No caching
- Deterministic
- Responsibilities :
-
interface ChunkProvider { Chunk getChunk(int cx, int cz); }
Implementation responsibilities:
- Manage region cache
- Generate region layers on demand
- Guarantee:
- Renderer can request any chunk
- Infinite world behavior
- No out-of-rect exceptions
- Region boundaries never leak
-
interface Renderer { void render(Camera camera); }
Implementation:
- LwjglRenderer
- Responsibilities :
- Near-field chunk meshes
- Far-field LOD (heightmap-based meshes)
- Texture atlas sampling
- Treat core as a black box
- Responsibilities :
- LwjglRenderer
- No global “world heightmap”
- No fixed generation window exposed
- No mutation of world data during generation
- All layers must be:
- Inspectable
- Cacheable
- Serializable
- Region boundaries must never leak outside core
- Architecture must support:
- Distant Horizons
- Streaming
- UE5 replacement later
- FastNoiseLite.java
- atlas.png
- atlas.json These must be used but must not dictate architecture.
- Correct package structure
- Core interfaces
- Streaming-ready region-based design
- Region cache strategy
- Focus on correctness first, then performance, then long-term scalability.
I want to start this project the right way so it can scale to a real Minecraft-like engine with distant horizons.
To run 2D Viewer :
.\gradlew.bat runBiomeViewerTo run the 3D World :
.\gradlew.bat run
