Sarah Yoo

Structure Generation System

To make my voxel world look more alive, I added simple structures like grass and trees. These sructures are randomly spawned when a new chunk is created, before building the chunk's mesh.

Structure Generation

Spawning a structure is done by modifying the block types within a chunk. For example, generating a tree means setting a few blocks to Wood and others to Leave. Furthermore, before placing any structure, I check if another structure already exists at the intended location to prevent overlap.

Problem in Cross-Chunk Structure Generation & My Solution

You may see a structure like this: Incomplete Tree

This type of cut off structure occurs when a structure is generated across chunk borders, but the chunk hasn't been loaded yet or the boundary information of the block arrays is not updated.

This is a case like this: Cross-Chunk Visualization

Recall that I've made my voxel engine so that each chunk has a blocks array that is extended by 1 block in the x and z directions. These extra boundary layers are used to store block informations of neighboring chunks at the borders.

Since each chunk can only manage its own block data, updating a block at a chunk border does not automatically update the corresponding boundary data in adjacent chunks. Or, there isn't a chunk to update the block information because the chunk hasn't been created yet.

Therefore, the solution is manually updating the boundary block data in both the current chunk and its adjacent chunks, as well as caching the block update for unloaded chunks.

Code Implementation

This ensures block data is updated correctly for all involved chunks:

Drop to see the code

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;
}