Voxel Engine

Structure Generation

Structure Generation

The terrain generation system creates the landscape, but it still feels empty without anything on top of it. To make the world feel more alive, I added some structures in the world, such as grass and trees.

Structures are generated during the chunk generation, before the chunk mesh is built. At the voxel level, there is nothing fundamentally different about a tree from the terrain itself: a tree is simply a collection of block types arranged in a particular pattern.

For example, generating a tree means placing Wood blocks for the trunk and Leaves blocks around its top.

Structure placement also needs to account for existing blocks. Before placing a structure, the intended region is checked to avoid overlapping another structure or replacing terrain that should remain intact.

The Cross-Chunk Problem

Incomplete Tree

Adding structures exposed a problem that did not exist when terrain was generated independently inside each chunk.

A structure does not necessarily fit within a single chunk. A tree near the edge of a chunk can have part of its trunk or leaves extending into the neighboring chunk.

This can produce an incomplete structure like the photo above when the neighboring chunk has not been created yet, or when its boundary data has not been updated after the structure is placed.

Cross-Chunk Visualization

This was particularly relevant because each chunk already uses an extended block array. As described in the mesh optimization section, the array contains an additional boundary layer in the x and z directions to hold neighboring block data.

The problem is that this boundary data is only a copy of the neighboring chunk's blocks. Changing a block in one chunk therefore does not automatically change the corresponding boundary data in another chunk.

The problem becomes even more interesting when the neighboring chunk does not exist yet. There is no block array to update. So how did I solve it?

Keeping Cross-Chunk Data Consistent

My solution was to treat a structure placement as a world-level block update rather than an update belonging exclusively to the chunk where generation started.

When a structure crosses a chunk boundary, the affected boundary data is updated in the neighboring chunk when it exists. If that chunk has not been created yet, the block update is cached and applied when the chunk is eventually generated.

This keeps the actual block data and the neighboring boundary layers consistent regardless of when the chunks are created.

My Code Implementation

bool ChunkManager::set_block(ivec2 chunk_id, ivec3 block_index, BlockType type) {
	Chunk* chunk = get_chunk(chunk_id);

	if (chunk == nullptr) {
		//if the chunk doesn't exist, cache the block update
		unloaded_blocks[chunk_id].push_back({ block_index, type });
	}
	else {
		//updates block type for the current chunk
		chunk->set_block(block_index, type);
	}
	
	// if the block is at the chunk's edge along the X axis (left and right edges)
	if (block_index.x == 1 || block_index.x == 16) {
		ivec2 offset = ivec2(0.0);
		// determine direction: -1 for left neighbor, +1 for right neighbor
		offset.x = (block_index.x == 1) ? -1 : 1;

		//adjacent chunk's id
		ivec2 adj_id = chunk_id + offset;

		// determine the corresponding block index in the neighboring chunk
		// (17 is the right boundary in neighbor; 0 is the left boundary)
		ivec3 adj_block_index = block_index;
		adj_block_index.x = (block_index.x == 1) ? 17 : 0;

		auto adj = chunks.find(adj_id);
		if (adj != chunks.end()) {
			// if neighbor chunk is loaded, update its boundary block
			adj->second.set_block(adj_block_index, type);
		}
		else {
			// If neighbor chunk is not loaded, cache the block update
			unloaded_blocks[adj_id].push_back({ adj_block_index, type });
		}
	}
	
	if (block_index.z == 1 || block_index.z == 16) {
		//repeat the same logic for the Z axis (front and back edges)
		... 
	}
	
	return true;
}

The result is that structures can span chunk boundaries without being cut off by the order in which chunks happen to be generated.

More importantly, this exposed a broader property of the voxel world: chunks may be spatially independent for storage and rendering, but they are not completely independent from the perspective of world state.

NextBlock Placement and Breaking (DDA Raycasting)