Sarah Yoo

Optimizing Mesh: Rendering Only Visible Faces

In a voxel world, each block has six faces. But, rendering all those faces would be a huge waste. Most are hidden by neighboring blocks, so to keep performance smooth, I render only the visible ones.

A face is considered visible if there is no solid block next to it in that direction. For instance, the top face of a block at the surface, or the side of a block beside an air block or a block with transparency (e.g. water) will be drawn.

Here's my code implementation:

Drop to see the code

void Chunk::build_chunk() {
	//check blocks where x and z range from 1 to 16 (excluding boundaries at 0 and 17).
	//It is to check only the current chunk's block data, not neighbor chunk's.
	for (int x = 1; x < width - 1; ++x) {
		for (int z = 1; z < length - 1; ++z) {
			for (int y = 0; y < height; ++y) {
				const Block &current = blocks[x][y][z];
				if (current.type == Air) {
					continue;
				}
				int h = height_map[x][z];
				bool am_i_transparent = has_transparency(current.type);
				ivec3 pos = ivec3(x, y, z);
				BlockType type = current.type;

				//add face only if the current block is 'solid' and the neighbor block has transparency.
				if (blocks[x - 1][y][z].type == Air || has_transparency(blocks[x - 1][y][z].type) && !am_i_transparent) {
					add_face(Left, type, pos);
				}
				if (y > 0 && (blocks[x][y - 1][z].type == Air || has_transparency(blocks[x][y - 1][z].type) && !am_i_transparent)) {
					add_face(Bottom, type, pos);
				}
				if (blocks[x][y][z - 1].type == Air || has_transparency(blocks[x][y][z - 1].type) && !am_i_transparent) {
					add_face(Back, type, pos);
				}
				if (blocks[x + 1][y][z].type == Air || has_transparency(blocks[x + 1][y][z].type) && !am_i_transparent) {
					add_face(Right, type, pos);
				}
				if (y < height - 1 && (blocks[x][y + 1][z].type == Air || has_transparency(blocks[x][y + 1][z].type) && !am_i_transparent)) {
					add_face(Top, type, pos);
				}
				if (blocks[x][y][z + 1].type == Air || has_transparency(blocks[x][y][z + 1].type) && !am_i_transparent) {
					add_face(Front, type, pos);
				}
			}
		}
	}
}

Now the invisible sides of chunk are not drawn.
Chunk Surface

While this is a basic optimization, it lays the groundwork for more advanced techniques like greedy meshing or frustrum culling for even better performance.

Checking Neighboring Chunk Data

Normally, each block face (not only top but also front, back, bottom, left, and right faces) would be rendered. But in a procedurally generated world where chunks are created adjacently, rendering all side faces causes overlapping walls between chunks.

To address this, I extend each chunk's block array by one block in x and z directions. So, the block array size changes from 16 x 16 x 16 to 18 x 16 x 18. I use this outer layer to store neighboring chunk data and check it when deciding whether to draw a face at the edge of a chunk.

I haven't extended the array in the y direction since I don't create downward chunks (yet). I plan to add that when implementing features like caves.

Neighboring Chunks