Under Construction

Click to Continue

Minecraft Clone – Blocks and Chunks

TODO

About a year ago I developed a Minecraft clone in Unity that features fully destructible and infinite terrain. This was made possible through the use of voxels. In this post I cover how voxels are represented and the process of converting them into renderable meshes.

Blocks

Minecraft represents the terrain and structures of its world using unit-sized voxels called blocks. Every block has a type, such as dirt or stone. While the real Minecraft has hundreds of block types, my clone has a mere 24 block types.

Each block type has a variety of properties associated with it, such as blast resistance and sound effects. To store these properties in my clone, I created a class that derives from Unity’s ScriptableObject. This allows me to create and edit block types like any other asset, making it easy to add new block types without having to change code.

List of all block types used (as seen in the Unity editor)

The full list of properties stored per block type is shown below. Note that not every property is relevant to every block type.

int index;
string name;
int blockBlastResistance;

// Textures
Vector2Int[] atlasPositions;

// Effects & Audio
AudioClip digClip;
AudioClip[] stepClips;
ParticleSystem.MinMaxGradient breakParticleColors;

// Boolean Flags
bool isTransparent;
bool isPlant;
bool isBillboard;
bool affectedByGravity;
bool isSourceBlock;
bool isFluid;
bool mustBeOnGrassBlock;

When storing the blocks in memory, I represent each block as an integer corresponding to the index of its type. For example, a dirt block is represented by the number 2 and sand by the number 7. Because I’m handling so few types, I could actually store them as a byte instead of an int to save on memory, but it wouldn’t make a significant difference for this project.

A consequence of storing blocks by their type is that every block of the same type is identical. However, the memory efficiency makes this worthwhile.

How would you represent blocks with unique states?

In Minecraft, there is an additional concept of block states. As the name implies, this allows blocks of the same type to have different states. This includes behavioral differences like a door being opened or closed, as well as visual differences like the type of wood a block is made of. Blocks with an inventory, such as chests, use a separate entity system to track the items stored in specific blocks.

Chunks

In order to create an infinite world on a device with not-so-infinite memory, I have to divide the world into finite chunks of blocks. At any given time, I only load chunks that are within a certain range of the player. As the player moves, chunks that enter this range are loaded while those that exit are unloaded.

Player moving with a render distance of three chunks.

Each chunk is represented as a 3D integer array, the dimensions of which are fully configurable in the Unity editor.

Minecraft uses chunks of size 16x16x256, however this gets divided into more manageable 16x16x16 sections when rendering. After playing around with different sizes, I settled on 16x16x24 as a good size for my world type.

Unlike Minecraft, my world doesn’t have any arbitrary height limits. Therefore, you can go in any direction indefinitely, not just horizontally.

Rendering a Chunk

So at this point I have a chunk represented by voxel data (3D integer array). However, I need to convert it to Unity’s triangle mesh in order to render it. The process of converting voxel data to meshes is known more generally as isosurface extraction. Some algorithms like Marching Cubes and Dual Contouring can even create smooth meshes from “blocky” voxel data. However, I actually want the blocky look that gives Minecraft its unique aesthetic.

I played around with a few different meshing methods, starting with the most basic.

One Cube Per Block

The naive way to render a chunk is to simply place a cube mesh at each block in the chunk. This is straightforward and works great! …at least for a handful of blocks. If you try to render thousands of blocks you will soon be met with disgraceful frame rates as your computer struggles to render each individual cube.

Left: Filled chunk with a cube mesh for each block. Right: Wireframe version of chunk.

Cube Culling

A simple optimization is to only render blocks that could potentially be seen by the player. This means culling any blocks surrounded on all 6 sides by opaque blocks. It’s important to consider transparent blocks (like water and glass) to avoid incorrectly culling visible blocks.

Left: Chunk with culled blocks highlighted in red. Right: Chunk with culled blocks removed.

Quad Culling

Now we are left with only the blocks that can be seen by the player. However, each of these blocks has 6 quad faces, most of which aren’t visible to the player. Therefore, the mesh can be futher optimized by removing all these interior quads.

Left: Chunk with culled quads highlighted in red. Right: Chunk with culled quads removed.

By this point my chunks were rendering nicely, so I decided to move on to texturing.

Are there more efficient meshing algorithms?

I later found an algorithm called Greedy Meshing, which futher reduces the mesh size by combining adjacent quads into larger rectangles. However, my meshing algorithm was already performant enough for my use case.

Reducing Draw Calls

While the size of the chunk mesh is now reasonable, it still requires hundreds of draw calls. This has a major impact on performance.

I can reduce each chunk to just one draw call by combining all the quads into a single mesh rendered with a single material. In order to allow each block to keep its unique texture, I use an atlas containing all block textures. Each quad then uses its UVs to index into this atlas.

Example of atlas texture being mapped to quads.

Billboard Blocks

So far I’ve only discussed rendering blocks as cubes. However, many block types like flowers are rendered using two perpendicular quads (billboards) that intersect at their center.

I consider these billboard blocks to be transparent, and will always render both of its quads if the block is visible. Otherwise, the meshing algorithm works the same as it does for cube blocks.

Examples of cube-rendered blocks (left) and billboard-rendered blocks (right).

Infinite Chunks

Performing opertaions within a single chunk is simple, as it only involves iterating over a 3D array. However, the introduction of infinite chunks makes this more difficult. It is no longer sufficient to iterate over a single chunk, as I also have to check the boundary blocks of adjacent chunks.

Data Structure

I have a WorldChunk class that acts as a wrapper around the 3D array of block types. I then use a Dictionary to store the loaded world chunks. The key for a chunk is the coordinate of its minimum corner.

Because the chunk size is consistent per dimension (x/y/z), it is easy to find the minimum corner given an arbitrary position within a chunk. In addition, since each minimum corner is separated by the same interval, I can quickly find adjacent chunks by adding/subtracting the chunk size from a chunk’s key.

Example of finding adjacent chunks (red) near the player's current chunk (blue).

Memory

I opted to not implement world saving for my Minecraft clone, as it didn’t seem worth the extra effort. Therefore the world chunks are only stored in RAM while the game is running.

For efficiency, the only chunks that are loaded at any given time are those within the player’s render distance or those that have been modified by the player. All other chunks are unloaded to free up resources. Since the world generation is deterministic, unmodified chunks can simply be regenerated if necessary.

So technically there’s no cap on how many chunks you can explore (ignoring numerical precision), though the amount of chunks you can modify is limited by your system’s RAM.