Introduction
This series of posts will focus on creating a voxel engine, from scratch, based on BabylonJS for the low-level 3D routines support.
To begin, here is in the video below, the first target we will have to reach, in order to manage the rendering of the world.
So what is a voxel ?
To keep it simple, a voxel is in 3D what a pixel is in 2D. It is value in grid, in a 3D space.
Strictly speaking, the voxel is like a pixel, in the meaning that it has only one value, its color.
Voxel engines generally have a little more flexibility in the degree to which the display of a voxel is done. It can display a cube single colored, or textured as in Minecraft.
So displaying cubes is not a big deal, isn't it ?
Short answer : Yes... and no.
A 3D engine, in order to keep a good frame rate, can apply many optimizations to the 3D scene to render.
It can hide the non visible objects, or simplify the objects according to the camera distance.
The problem with voxels is that you will have a very large quantity of cube, so even if you try to hide some of them, you will quickly struggle in rendering speed.
Moreover, a cube is a simple geometric shape, and therefore, simplifying this object cannot be done without severely deforming it. Remove one node and the cube becomes anything you want except... a simpler cube.
So okay, but where to start then?
Let's start with something basic, which is to define some target functionalities that we are going to implement.
We're going to take our inspiration from the way Minecraft handles the rendering of worlds in the game, at least in the early versions of the game.
We will try to use as few technical terms as possible, just the bare minimum required, in order to keep all the explanations understandable to everyone.
World structure
The world
A world represents a set of voxels that it will be possible to display.The world is divided into regions.
The region
A region represents a piece of the world. Each region has the same number of voxels. A region is also represented by a 3D coordinate. A region is composed by a data chunk.
A chunk
A chunk is composed of a set of voxels, in a 3-dimensional grid, where each dimension is the same size. This can be simplified as a cube filled with small cubes.
Let's assume for example that a data chunk is composed of 3 dimensions of size 32. A region thus has 32*32*32 voxels, a total of 32768 voxels.
If our world has 100*100 regions per layer and let's say 3 layers of height, we will have a total of 100*100*3 regions, so 30000 regions.
Our world will thus have a total of 100*100*3*32768 = 983 040 000 voxels. Our very small world already has close to a billion potential voxels.
Block definition
Our voxel, in our engine, will be presented as a block, more complex in structure than a simple 3D point.
export type Block = {
name : string; // Block name
guid : string; // Unique global Id
uid : number; // Unique local id
sidesTex : [ // Array of textures
string, // BACK
string, // FRONT
string, // RIGHT
string, // LEFT
string, // TOP
string // BOTTOM
];
size: [ // Edges size
number, // WIDTH
number, // HEIGHT
number // DEPTH
];
type : string; // GAZ, LIQUID, BLOCK
opacity : number;
speed : number; // 0 - 1
};
So we have the smallest usable unit.
Each block will need some data to represent each side, for optimization purpose. Let's define an enum to represents sides.
export enum Side {
Left = 1 ,
Right = 2 ,
Forward = 4 ,
Backward = 8 ,
Top = 16 ,
Bottom = 32 ,
Z_Axis = 3 ,
X_Axis = 12 ,
Y_Axis = 48 ,
All = 63
}
Chunk definition
A chunk will store different kind of data, including the full and the optimized version of the blocks.
export type Chunk = {
position : Vector3 ; // 3D position in the world
size : number ; // Size of the chunk, default will be 32
data : Array<number> ; // The original data
dataSize : number ; // The number of non empty blocks
rcData : Array<number> ; // An optimized version of visible only visible data
rcDataSize : number ; // The number of visible blocks
hasRc : boolean ; // Define if a chunk has been optimized or not
};
1D array or the power of flattening everything
When dealing with Typescript / Javascript, it is easy to deal with array of array. It seems common to proceed like this.
But here, we need to keep in mind that performance will decrease rapidly as soon as we add new features, so we need to avoid wasting our precious frame per second by taking the easy way out.
Using a one-dimensional array to simulate a 3-dimensional access will always be faster. We will therefore use functions to simplify our work.
/**
* Convert a vector 3 coordinate to a flat array index
* @param x {number} The x coordinate
* @param y {number} The y coordinate
* @param z {number} The z coordinate
* @param size {number} The size of each dimension, the size is the same for each one
*/
export function vector3ToArrayIndex(x: number, y: number, z: number, size: number = 32) {
return (size * size * x) + (size * y) + z;
}
/**
* Convert a flat array index to a 3D coordinate representation
* @param index {number} The array index
* @param size {number} The size of x,y,z dimension
*/
export function arrayIndexToVector3(index: number, size: number = 32) {
return new BABYLON.Vector3(
(index / (size * size)) >> 0,
((index / size) % size) >> 0,
(index % size) >> 0
);
}
This will conclude our introduction. In the next post, we will see how to render our blocks using Babylon Js, and the minimum of 3D terminology necessary to understand the next posts.
Enjoy !
Top comments (3)
Great tutorial! I love learning from all of these different projects. May I share mine? I actually started a voxel engine tutorial here, and your page has helped me with understanding something. So I'll write about that next!
Really nice blog entry! I can't wait to read the other parts.
Thank you ! I'm on it ;-)