A voxel sandbox game written from scratch in Java, using LWJGL for windowing and OpenGL for rendering. Procedurally generated terrain, water, trees, wandering pigs, block breaking, frustum culling and a distance-fogged horizon — no game engine involved.
- Features
- Screenshots
- Getting started
- Controls
- How it works
- Project layout
- Textures
- Tuning the world
- Contributing
- License
- Procedural terrain — multi-octave Perlin noise drives a heightmap across a 16 × 16 grid of chunks.
- Biome-ish decoration — trees and large trees seeded by probability, with an ocean level that floods low ground.
- Block breaking — hold left mouse on a block to damage it through six crack stages until it shatters. Different materials have different strengths.
- Water — translucent liquid blocks, a swimming state with reduced movement speed, and a blue underwater fog effect.
- Wildlife — pigs wander the world as gravity-affected entities.
- Lighting — a directional sun with ambient/diffuse/specular terms, plus point lights from lamp blocks.
- Performance work — frustum culling, hidden-face culling, and a cache of visible surface blocks per chunk so buried geometry is never submitted.
- Original, generated art — every texture is produced from noise and maths by a script in this repo.
The generated world from above — elevation steps, forests, lakes and a wandering pig:
| Underwater | Block targeting |
|---|---|
You need JDK 21 and Maven. Nothing else — no database, no server, no API keys.
mvn compile exec:execThat's the whole thing. Maven detects your platform, pulls the matching LWJGL natives, compiles, and launches the game with the right JVM flags.
Building a standalone jar
mvn package
java -jar target/cubeworld.jar # Linux / Windows
java -XstartOnFirstThread -jar target/cubeworld.jar # macOSmvn package produces a fat jar with every dependency and native bundled in.
LWJGL natives are selected automatically by Maven profiles that key off os.name / os.arch. macOS
(Intel and Apple Silicon), Linux (x86-64 and arm64) and Windows (x86-64) are wired up.
On macOS, GLFW must own the very first thread of the process, so the game has to run with
-XstartOnFirstThread. mvn exec:exec supplies that for you. This is also why the project uses
exec:exec rather than exec:java — the latter runs inside Maven's own process, where the flag cannot be
applied, and the JVM segfaults during GLFW initialisation.
| Input | Action |
|---|---|
W A S D |
Move |
| Mouse | Look around |
Space |
Jump |
Left Ctrl |
Sprint |
Left Shift |
Sneak (prevents walking off ledges) |
| Left mouse button | Break the block you are looking at |
R |
Teleport to y = 75 (debug: useful for surveying the world) |
Esc |
Quit |
Frame loop. Game owns the window, the delta-time clock and the FPS counter. Each frame it clears the
buffers and calls World.update(), which advances the skybox, refreshes the view frustum, renders the
chunks around the player, updates animals, and finally updates the player.
Chunks. A Chunk is a 16 × 16 column of blocks stored in a HashMap<Integer, Block> keyed by a packed
integer coordinate. Terrain height comes from NoiseGenerator.generateTerrainHeight(x, z, octaves, persistence, scale). Anything below the ocean level (y = 7) is filled with water; the surface block is
grass, everything beneath it is stone.
Render ordering. Chunks are sorted by distance from the player and drawn far-to-near, in three passes — solids, then transparent blocks (leaves), then liquids (water). The player's own chunk is drawn last in each pass. This ordering is what makes alpha blending on water and leaves look correct.
Culling. Two layers. Frustum extracts the six clipping planes from the projection × view matrix and
rejects blocks outside them. Separately, Block.isVisible() checks the six neighbours and skips any block
that is fully enclosed, so only the surface shell of the terrain is ever submitted to OpenGL. Each chunk
caches that visible set and invalidates it when a block is removed.
Block hierarchy. Block is abstract and defines getStrength() and getMaterial(). A Material
carries one texture id per cube face, so grass can have a green top, dirt bottom and speckled sides.
Three subclasses control which render pass a block belongs to:
Block (abstract)
├── SolidBlock → Grass, Stone, Wood, Lamp
├── TransparentBlock → Leaves
└── LiquidBlock → Water
Every face maps texture U to the face's horizontal axis and V to world Y, so directional art such as bark grain runs the same way on all four sides of a block.
Entities. Entity handles gravity (−30 u/s², terminal velocity −50 u/s), AABB collision against the
block map, and swimming/underwater state. Player extends it with camera control and input; Animal
extends it with a textured cube body and wandering behaviour.
Picking. Camera.getLookingAtBlock() marches a ray forward from the camera in 0.1-unit steps up to 5
units, returning the first non-liquid block it hits. That block gets a white wireframe outline and receives
damage while the mouse is held.
src/main/java/com/clouddrop/
├── Main.java entry point
├── Game.java window, frame loop, GL setup
├── Window.java GLFW window wrapper
├── Camera.java view matrix, fog, crosshair, ray picking
├── Chunk.java 16×16 block column, generation and render passes
├── Skybox.java sun direction and sky colour over the day cycle
├── Material.java per-face texture ids
├── Textures.java texture id registry
├── Location.java x/y/z plus owning world and chunk
├── worlds/World.java chunk grid, animals, render ordering
├── objects/ Block hierarchy, PointLight
│ ├── blocks/ Grass, Stone, Wood, Leaves, Water, Lamp
│ ├── templates/ Tree, LargeTree structure generators
│ └── entities/ Entity, Player, Animal, animals/Pig
└── utils/
├── NoiseGenerator.java Perlin noise with octave support
├── Frustum.java six-plane frustum extraction
└── TextureLoader.java classpath → stb → GL texture
tools/TextureGenerator.java generates every PNG in src/main/resources
src/main/resources/ the generated 16×16 textures
docs/screenshots/ images used by this README
Every texture is generated, not drawn by hand or copied from anywhere. tools/TextureGenerator.java
produces all fifteen PNGs from seeded value noise and simple maths — mottled grass, tiling bark grain,
concentric end grain on logs, sine-ripple water, radial lamp glow, and a six-stage crack overlay. The noise
is sampled with wraparound so each tile meets its own edges seamlessly when blocks repeat.
The generated files are committed, so you do not need to run this to play. To re-roll the art:
mvn exec:exec@textures
# or, without Maven:
java tools/TextureGenerator.java src/main/resourcesChange a SEED constant in the generator and re-run to get a different variation in the same style. All
generated assets are covered by this project's MIT license.
Most of the interesting knobs are constants you can edit directly:
| What | Where |
|---|---|
| World size (16 × 16 chunks) | World.init() |
| Render distance (3 chunks) | World.renderChunksAroundPlayer() |
| Ocean level, tree/pig spawn rates | Chunk.generateChunk() |
| Terrain octaves, persistence, scale | Chunk.generateChunk() → generateTerrainHeight |
| Gravity, terminal velocity | Entity |
| Walk / sprint / sneak / swim speeds | Player.processInput() |
| Fog density and range | Camera.initFog() |
| Day length (20 min) | World.dayDurationMillis |
Worth knowing before you dig in — these are all open for contribution:
- The world is a fixed 16 × 16 chunk grid. There is no chunk streaming, so you can walk off the edge.
- The day/night cycle is implemented in
Skyboxbut pinned to midday —World.update()overwrites the computedtimeOfDayFactorwith a hard-coded0.6f. Remove that line to let time run. - Rendering uses the OpenGL 1.x fixed-function pipeline (
glBegin/glEndimmediate mode) rather than VBOs and shaders. It is simple to read but caps the achievable frame rate. - Blocks can be broken but not placed.
- There is no world persistence; every launch generates a fresh world from a time-based seed.
- Lamp point lighting exists (
Chunk.renderLighting()) but is not called from the render loop. The generator also uses lamp blocks to pave the sea floor, which is why the shallows glow amber.
Contributions are welcome. See CONTRIBUTING.md.
MIT — code and generated assets alike.
Cubeworld is an independent, non-commercial learning project, not affiliated with or endorsed by any other game or its publisher.