LOD (Level of Detail) For Voxel Engines
Header Image

Voxel Engine LOD (Level Of Detail) Explained

LOD, or Level of Detail, is a required algorithm for building high-performance voxel engines. LOD (Level of Detail) is a rendering technique that will draw chunks that are farther away from the camera by using less detail. If your chunk size is 64x64x64 voxels across X, Y and Z axis, then your next reduced level of detail chunk could be represented by 32x32x32.

Your next LOD would be 16x16x16, then 8x8x8, 4x4x4, and 2x2x2. Finally 1x1x1 LOD can be used where an entire chunk rendered far away from the player could be represented by just 1 voxel, but this depends on implementation. Often, 4 to 5 levels of detail is enough for rendering horizons populated by massive landscapes.

But then again, this is a decision you will make based on the visual style of your voxel engine. How LOD is handled has a lot to do with what makes your voxel engine look distinct from other engines.

How To Downsample A LOD Chunk

For each level of detail, you will use original voxel data set (64x64x64, for example) and determine whether the 32x32x32 equivalent should have a voxel within its own set or not. Usually this is determined by checking if a 2x2x2 area within the 64x64x64 chunk contains at least 1 voxel, if it does, that means the voxel that correlates to that scaled position within the new 32x32x32 chunk will be set to solid.

If all voxels within 2x2x2 inside 64x64x64 chunk are empty, that means the voxel for the equivalent location within 32x32x32 chunk will be empty.

Here's basic pseudo code that can be used to downscale a larger chunk to a smaller chunk:

void downsampleChunk(int orig[64][64][64], int down[32][32][32]) {
        for (int x = 0; x < 32; ++x)
            for (int y = 0; y < 32; ++y)
                for (int z = 0; z < 32; ++z) {
                    int count = 0;
                    for (int dx = 0; dx < 2; ++dx)
                        for (int dy = 0; dy < 2; ++dy)
                            for (int dz = 0; dz < 2; ++dz)
                                count += orig[x * 2 + dx][y * 2 + dy][z * 2 + dz];
                    down[x][y][z] = (count >= 4) ? 1 : 0;
                }
    }
    
    int main() {
        int orig[64][64][64] = { /* ... Fill this with your voxel data ... */ };
        int down[32][32][32] = {0};
        downsampleChunk(orig, down);
        return 0;
    }

You're looking for this LOD tutorial probably because you tried to render an entire voxel world consisting of hundreds of chunks in a for loop. Either the rendering was slow or you ran out of RAM when populating GPU vertex buffers during initialization.

Here I should also mention that in many cases the CPU will have less RAM memory than the GPU. This means, if your voxel engine crashes during initialization of millions of voxels, it doesn't mean your GPU still can't handle it.

If you run into this problem, you need to start thinking how to "funnel" your voxel dataset to the GPU, without allocating memory for all voxels in the world on the CPU. OpenGL has functions for doing that, instead of allocating an entire data vector at one time, which is unrealistic and often undesirable.

pyramid and a castle, with a dynamic sun, a voxel engine screenshot

This is a visualization of LOD algorithm. Voxels in chunks farther away are rendered smaller only to demonstrate how LOD works. In correct implementation, the black voxels would be correctly adjusted to fully occupy this space, and to correct this visualization, the black voxels would consist of 32x32x32 chunks and not 16x16x16.

Note, this is just a test visualization. Normally the next smallest level would actually be 32x32x32, something that can be adjusted in the shader using a simple uniform variable.

The voxel engine depicted in the screenshot above uses 64x64x64 voxels per chunk. These are the closest chunks with dirt-colored voxels. The LOD areas can be determined by calculating distance from player's current position to the center of the chunk.

It's possible to have more than just 2 levels of LOD. And if your most detailed LOD level consists of 64x64x64 voxels per chunk, you can even have up to 7 levels, with dimensions stated in the first paragraph of this tutorial. LOD need not precisely halve at each subsequent level, though this is advisable due to memory layouts. Computer memory tends to favor configurations in powers of two.

Loading And Off-loading Chunks Dynamically

In first version of your voxel engine you will probably be drawing all chunks with a triple for-loop. You will then apply field of view frustrum culling and reduce the visible set of chunks to a smaller list. You would then mark some chunks as visible and others as invisible, and only render the visible chunks based on a simple if statement.

The above approach is sufficient for small worlds, but ideally you will want to implement dynamic LOD by determining visible chunks and packing vertex buffer arrays dynamically in real-time based on visible chunks, instead of simply using an if statement (in which case even invisible chunks are still uploaded to the GPU). The difference with a completely dynamic LOD is that you will be packing vertex buffers dynamically per frame, instead of keeping entire world on the GPU.

But this introduces another problem. Loading and offloading chunk data from hard drive may be fast. But you still need to manage a list of chunks which are currently visible or invisible and then dynamically pack vertex data into them to send to the GPU.

In addition to that, you will also need to re-pack vertex buffers based on their current LOD per chunk. Multiple LOD levels can still be packed into a single vertex buffer for the GPU. But updating them when a chunk's LOD changes based on player location in the world might introduce a small hiccup. If you're processing LOD changes in the same thread, this hiccup might become visible especially on lower end PCs.

The solution to overcoming this obstacle is to use multi-threading. You would create a second thread responsible for updating the vertex buffers, and maybe even loading and offloading chunks from hard drive. This would eliminate stuttering caused by repacking vertex buffers, which is usually a quick process, but in cases of a more complex voxel world might contribute to sporadic drops in frame rate.

Dynamically Switching LOD With Smooth Transition

When player travels in the world, each chunk will update its current level of detail based on distance from player. This can produce a side effect where a chunk currently using one level of detail, becomes farther away from player or closer to the player, and switches to a lower or higher resolution chunk dynamically. This change can be visually observed on the screen. Ideally, you will want to create a smooth transition between chunks that switch from one LOD level to another. This can be achieved by alpha blending between LODs.

Latest Voxel Engine Tutorials