A high-performance voxel engine for Unity built on the Job System and Burst Compiler.
Binary face culling · Greedy meshing · Ambient occlusion · Flood-fill sun lighting · Fully parallel
This project was inspired by this fantastic breakdown of greedy meshing. Highly recommended watch before diving into the code.
I'm writing a detailed blog series on how every system here was built. Follow along at shipthecode.dev.
- Binary face culling uses 64-bit bitmask operations to reduce visible face detection to a handful of bitwise instructions per slice
- Greedy meshing merges adjacent coplanar faces into the largest possible quads, reducing triangle count by up to 50%
- Custom shader encodes voxel ID and light level per-vertex instead of per-face, which allows the greedy merge step to be more aggressive than a naive implementation
- Ambient occlusion is baked directly into the mesh at generation time with zero runtime cost
- Flood-fill sun lighting propagates 4-bit (0-15) light values using a queue-based flood fill running as a Burst-compiled job
- Live voxel editing lets you add or remove voxels at runtime and the lighting and mesh update automatically
- Jobs + Burst throughout: voxel generation, bit matrix building, lighting, and meshing all run as Burst-compiled
IJobstructs on worker threads - Perlin noise terrain included out of the box, easy to replace by implementing a single interface
The world is divided into chunks, fixed-size 64×64×64 voxel volumes. Only chunks near the player are loaded at any given time. Each chunk is generated, lit, and meshed independently, which is what makes the whole pipeline parallelisable.
Each chunk goes through a structured async pipeline before it appears on screen:
Voxel Generation (IVoxelsGenerator)
│
▼ [parallel]
┌───────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ VoxelBuffer job │ │ BitMatrix job │ │ Local Sun Light │
│ (pack voxel IDs │ │ (3×64 ulongs, │ │ (flood fill from │
│ into uint[]) │ │ face culling) │ │ top of chunk) │
└───────────────────┘ └──────────────────┘ └────────────────────┘
│ │ │
└───────────────────────┴────────────────────────┘
│
▼
Neighbor Light Seeding
(copy border values from
already-loaded neighbors)
│
▼
Neighbor Flood Fill job
│
▼
Mesh Generation job
(binary cull → greedy
merge → AO → encode)
│
▼
ChunkGameObject updated
Three 64×64 bitmasks (one per axis) represent which voxel slots are solid. A single bitwise shift and AND produces the set of faces that need to be drawn, with no per-voxel comparisons.
visible_faces = solid_mask & ~(solid_mask >> 1)
After culling, the algorithm scans each 2D slice of the chunk and merges adjacent visible faces into the largest rectangles possible. Faces with different lighting or AO values still can't be merged.
A standard greedy meshing implementation can only merge faces that share the same block type, since the texture is typically set per-face. The custom shader here encodes the voxel ID per-vertex instead, so the GPU looks up the correct texture for each pixel within a merged quad. This means faces of different block types can be merged into a single quad, pushing the triangle reduction further.
AO is computed at mesh generation time. For each vertex, the four surrounding corner voxels are sampled and the occlusion value is packed directly into the vertex data. The shader applies a cosine curve for a soft look:
ao = 1.0 - cos((light / 15.0 * PI) / 2.0)
finalColor *= lerp(1.0, ao, _AOStrength);No screen-space passes, no ray marching.
Light values are 4-bit integers (0-15). Sun light enters from the top of each chunk and propagates downward and outward using a queue-based flood fill. When a voxel is added or removed, only the affected region is recalculated. Darkness propagates first, then light re-floods from surviving sources.
Every expensive step runs as a Burst-compiled job on Unity's worker threads. Multiple chunks generate simultaneously, and the phases within each chunk (voxel buffer, bit matrix, lighting) run in parallel via combined JobHandle dependencies.
- Unity 6000.0 or later
- Universal Render Pipeline (URP)
- Packages:
com.unity.entities,com.unity.inputsystem(pulled in automatically)
Add the package via the Unity Package Manager using the git URL:
https://github.com/Seremonik/Unity-Voxel-Engine.git
Or clone the repository and add it as a local package:
- Clone this repo anywhere on disk
- In Unity: Window → Package Manager → + → Add package from disk
- Select
package.jsonfrom the cloned folder
- Create a new URP project (or use an existing one)
- Add a
VoxelWorldcomponent to a GameObject in your scene - Assign an
EngineSettingsScriptableObject (Assets → Create → VoxelEngine → Engine Settings) - Implement
IVoxelsGeneratorto define your terrain, or use the includedHillsVoxelsGenerator(see Writing a Custom Voxel Generator) - Call
voxelWorld.SetPlayerChunk(playerChunkPosition)each frame to drive chunk loading (without this call no chunks will be generated)
EngineSettings exposes three properties:
| Property | Description |
|---|---|
AO Strength |
Controls how strong the ambient occlusion effect appears (0 = off, 1 = full) |
World Radius |
How many chunks to load around the player |
Max Jobs Per Frame |
Limits how many chunk generation jobs are scheduled each frame |
The included sample (Samples/Basic Example) gives you a fully playable scene out of the box.
Controls:
| Key | Action |
|---|---|
WASD |
Move |
Mouse |
Look |
Space |
Jump |
Left Click |
Remove voxel |
Right Click |
Place voxel |
B |
Lock / unlock cursor |
C |
Freeze movement |
F1 |
Toggle debug view |
F2 |
Toggle first / third person camera |
The sample scene is available directly in the package under Samples/Basic Example.
Implement IVoxelsGenerator and schedule a Burst-compiled job that fills the ChunkData.Voxels array:
public class MyGenerator : VoxelsGeneratorBase
{
public override JobHandle GenerateVoxels(ChunkData chunkData, int3 chunkPosition, JobHandle dependency)
{
var job = new MyGenerationJob
{
Voxels = chunkData.Voxels,
ChunkPosition = chunkPosition
};
return job.Schedule(dependency);
}
}Voxel values: 0 = air, 1+ = block ID (up to 255 block types). The engine handles culling, lighting, and meshing.
- Chunk-edge lighting: light values can be incorrect at the seam between two chunks. Top priority fix, coming in an upcoming update.
- Fix chunk-edge lighting seams
- Level of Detail (LOD)
- Serialization
- Dynamic lighting
- Complex world generation (biomes, caves, structures)
- Custom physics
I'm writing a detailed breakdown of how this engine was built: the math, the Job System patterns, the shader tricks, all of it. Follow along at shipthecode.dev.
MIT, see LICENSE for details.




