Voxel Engine

Mesh Optimization: Only Draw What Can Be Seen

The first major performance problem was straightforward: why generate geometry that can never be seen?

Every voxel has six faces, but most of those faces are completely hidden by neighboring blocks. Generating all six faces would create a large amount of geometry that contributes nothing to the final image.

The engine therefore uses two levels of face culling:

  • Hidden-face removal: prevents invisible faces from being generated in the first place.
  • Back-face culling: lets the GPU discard faces pointing away from the camera.

Mesh Optimization 1: Removing Hidden Faces

During chunk generation, each block's six neighbors are checked before a face is added to the mesh.

A face is generated only when the neighboring block does not completely cover it. A block exposed to air therefore contributes its visible surface, while the face between two solid blocks is never generated.

Transparent blocks such as water are treated separately because their surfaces can remain visible even when another block occupies the neighboring position.

The resulting mesh contains only the block faces that can potentially be seen.

Chunk Surface

This changes the amount of geometry dramatically. Instead of being proportional to all six faces of every block, the mesh size is only restricted to the visible surface area of the world.

Checking Neighboring Chunks

Face visibility becomes less straightforward at chunk boundaries.

A block at the edge of a chunk can have a neighboring block belonging to a different chunk. Without access to that neighboring data, the mesh generator would treat the missing block as empty and create a face that may actually be hidden.

To handle this, the block array is extended by one block in the x and z directions. A chunk is configured to contain 16 × 16 × 16 blocks, while the array used for mesh generation is 18 × 18 × 18.

The outer layer is used to contain data copied from neighboring chunks. This allows the same visibility test to work across chunk boundaries.

Neighboring Chunks

The y direction is not extended because the current world does not generate chunks below the surface. This can be extended later when features such as caves require downward chunks.

Mesh Optimization 2:Back-Face Culling

Removing hidden faces happens during mesh generation, but some of the remaining faces can still never be seen from the current camera position.

A triangle has a front and back side, and the side is determined by the winding order of its vertices. OpenGL can use this ordering to identify which side is facing forward.

Winding order

All front faces in the engine are wound clockwise, so I enabled back-face culling with:

glEnable(GL_CULL_FACE);
glFrontFace(GL_CW);

OpenGL can then discard triangles whose winding order indicates that they are facing away from the camera.

Back-face culling

In summary, the two optimizations happen at different stages in my engine:

  • Hidden-face removal happens during mesh generation on the CPU. The invisible geometry is never created.
  • Back-face culling happens during rendering on the GPU. Geometry that was generated but faces away from the camera is discarded.

Together, they reduce both the size of the mesh and the amount of geometry the GPU needs to process.

Dealting with Transparent and Double-Sided Faces

Back-face culling is not appropriate for every surface. Some objects need to be visible from both sides.

Water is one example. Its surface can be viewed from above or below, so removing all back faces would make part of the surface disappear.

These cases require the mesh to explicitly include the faces that should remain visible and use the appropriate winding order.

NextThe Secrete in Storing Many Textures