DEV Community

Cover image for How I made a multi-threaded voxel engine in TypeScript
Lucas Damian Johnson
Lucas Damian Johnson

Posted on • Edited on

How I made a multi-threaded voxel engine in TypeScript

Image description

Hello, I am Luke the creator of Divine Voxel Engine.

Divine Voxel Engine (DVE) is a truly multi-threaded voxel engine written in TypeScript that uses Babylon.Js.

In this post I just want to go over on a high level how it works.

Chunk Data And Shared Array Buffers

The engine stores the data for voxels in chunks. Chunks are segments of the world. By default they are defined as 16mx16mx16m areas. They are stored in a multi-level hash map. The first level being a region (512mx512mx512m area), then the next being a column (same width and depth as a chunk but as tall as a region).
The chunk data itself is just a SharedArrayBuffer. The engine uses the DataView object to read and write data to it. The buffer has a few headers such as its' position and height map.
The buffer is shared with all threads (workers) that need it. This way chunk building, water flow, light updates, and much more can be done in parallel.

Data Indexing

Since the data for the chunk is basically stored in a flat array we need to get the index for it ourselves.

The next code snippets are part of objects in the engine.

The first thing we do is get the chunk position:



 getChunkPosition(x: number, y: number, z: number) {
  this.__chunkPosition.x = (x >> 4) << 4;
  this.__chunkPosition.y = (y >> 4) << 4;
  this.__chunkPosition.z = (z >> 4) << 4;
  return this.__chunkPosition;
 }


Enter fullscreen mode Exit fullscreen mode

Then we get the relative voxel position in the chunk:



 getVoxelPositionFromChunkPosition(
  x: number,
  y: number,
  z: number,
  chunkPOS: Position3Matrix
 ) {
  this.__voxelPosition.x = Math.abs(x - chunkPOS.x);
  this.__voxelPosition.z = Math.abs(z - chunkPOS.z);
  this.__voxelPosition.y = Math.abs(y - chunkPOS.y);
  return this.__voxelPosition;
 }


Enter fullscreen mode Exit fullscreen mode

Then we get the index of the voxel in the chunks data:



const bounds = {
x : 16,
y : 16,
z : 16
};
export function getIndex(x: number, y: number, z: number) {
return x + y bounds.x + z bounds.z * bounds.y;
}

Enter fullscreen mode Exit fullscreen mode




Voxel Data

Voxel data is stored in 4 bytes.
The first two bytes are for light data. Light is stored as 4 bits for each light type. Light types include sun light and RGB light.
The next two bytes are for storing the voxel numeric id. After that the last bytes are for storing voxel shape state and voxel level as well as secondary voxel id for dual voxels (think of a plant underwater).

Mesh Building

All mesh building is done in the Constructor thread. A chunk is sent into a processor which will produce a template. The template is simply a list of all the exposed faces, the lighting gradient (and other gradients), texture uvs, and the actual voxel shapes themselves.
This template is then sent to a mesher which produces the actual mesh data for the chunk. Once it is done it just sends it to the render thread.
If you want to see how the light gradient and ambient occlusion is calculated check this out:
CalculateVoxelLight

Light Updates

Lighting for the voxel engine uses a simple breadth first search algorithm. If you want to see the code directly here it is:
RGB light
Sun Light
Sun light is a little different than RGB light. Since sun light does not deplete as it goes downward through air while RGB light does.

Inter Thread Communication

The engine is stitched together with another library I wrote called ThreadComm. It makes it easy to communicate between many threads and is set up to work in both the browser and the node environment.
ThreadComm allows you to create tasks queues which are basically functions in other threads which are executed from another thread and have a call back once all the tasks for that queue are finished.
The engine does not use one big queue for every update. It splits updates into regions. So, updates far away from one another will not be affected.

Conclusion

This was just a very high level overview of how DVE works. If you would like to know more or have specific questions let me know. DVE is licensed under MIT so you can use it for any project. However, all assets for the test worlds are owned and copyrighted by me.

Top comments (4)

Collapse
 
karmekk profile image
Bartosz Gleń

Wow, looks very interesting. I've starred the repository, I'll check out DVE and how it works.

If you don't mind a question, how long you've been working with computer graphics?
Your work is very impressive.

Collapse
 
lucasdamianjohnson profile image
Lucas Damian Johnson

Thank you!
I graduated college in 2020. I did not go to school for computer graphics but I complete a minor in computer science along with my degree. So, my background is mostly in computer science and art/design.
However, I've been working on video games since I was a young teen.

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

Be still my beating heart 💓 good job! I'm so into this sort of thing.

I'm trying to find details in this post about the runtime. It's not node.js I guess. Maybe graalvm or did I miss something?

Collapse
 
lucasdamianjohnson profile image
Lucas Damian Johnson

The voxel engine can run headless on a Node.Js server.
But the client/render runs in the browser or in an electron app.