Voxel Engine

Procedural World Generation

A voxel world is made up of blocks grouped into fixed-size chunks. Stitching many chunks together creates the terrain, while keeping the world divided into smaller regions makes it possible to generate and manage only the parts that matter.

chunk

From Noise to Terrain

A completely random terrain would look more like noise than a landscape. What was needed instead was a way to introduce variation while keeping nearby terrain correlated.

The terrain therefore starts with a height map: a 2D grid where each (x, z) position represents the elevation of the surface. The height values come from Perlin noise, which produces smoothly varying values rather than independent random values.

The height map only describes the surface. The actual voxel terrain is built by comparing each block's y position with its corresponding surface height.

for (int x = 0; x < width; ++x) {
    for (int z = 0; z < length; ++z) {
        for (int y = 0; y < height; ++y) {
            BlockType type = Air;
            int h = height_map[x][z];

            if (y > h && y <= water_level) {
                type = Water;
            }

            if (y == h) {
                type = Grass;
            }

            if (y < h) {
                type = Dirt;

                if (y < h - 5) {
                    type = Stone;
                }
            }

            if (y <= h && y >= h - 2 &&
                y + 1 < height && y + 1 <= water_level) {
                type = Sand;
            }

            blocks[x][y][z].type = type;
        }
    }
}

The result is a simple set of terrain rules: grass forms the surface, dirt sits underneath it, deeper layers become stone, and water fills the space below a fixed water level. Sand is added around shorelines where terrain meets water.

The interesting part is the separation between world generation and rendering. At this stage, the engine is only deciding what the world contains. The mesh system described earlier takes that block data afterward and determines what actually needs to be drawn.

Procedurally Generated Terrain

Making the World Larger

Procedural generation makes it possible to create terrain far beyond a manually designed map, but generating the entire world at once would defeat much of that advantage.

Chunks provide the boundary between the world and the renderer; only chunks within the player's render distance are kept active. A render distance of 1, for example, covers the current chunk and its eight immediate neighbors in a 3 × 3 arrangement.

The active chunks are stored in a visible_chunks collection and used as the set of terrain that needs to be rendered and updated.

This is a form of space partitioning. Dividing the world into chunks means the engine does not need to treat the entire terrain as one enormous object. Generation, mesh construction, GPU uploads, and rendering can all be limited to the region around the player.

My Code Implementation

/*
	creates and draw chunks within a render distance.
	this is called every frame.
*/
void Terrain::update() {
	//gets player's x and z position in chunk space.
	//note that x: width, z: length, y: height in my voxel engine.
	vec2 origin = {
		floor(player_pos->x / chunk_size),
		floor(player_pos->z / chunk_size),
	};

	//checks which chunks are within the render range.
	for (int x = origin.x - render_dist; x <= origin.x + render_dist; ++x) {
		for (int z = origin.y - render_dist; z <= origin.y + render_dist; ++z) {
			ivec2 chunk_id = { x, z }; //unique id of a chunk
			Chunk* chunk = ChunkManager.get_chunk(chunk_id);
			if (!chunk) {
			//creates a chunk if the chunk has not been created on that position.
				chunk = ChunkManager.create_chunk(chunk_id);	
			}
			//pushes the chunk into the vector that stores only visible chunks.
			visible_chunks.push_back(chunk);
		}
	}

	//render visible chunks.
	for (Chunk* c : visible_chunks) {
		c->render();
	}

	//empties visible_chunks vector.
	visible_chunks.clear();
}

The result is a world that can extend well beyond what is currently visible without requiring all of that terrain to exist in the active rendering system at once.

But chunking also creates an interesting boundary problem. A chunk may be generated independently, while the visibility of one of its faces depends on a block belonging to the next chunk over.

That connection between independently generated chunks is where the terrain system meets the mesh optimization system described earlier.

NextStructure Generation