Voxel Engine - Advice on performance - Have I reached JS/Babylon's limit?

I have been working hard on building a voxel engine with Babylon.Js. It is free for anyone to use.
Basically I got 95% of it figured out. I just want to ask some questions.

These questions are related to the voxel engine but also the limits of Babylon.Js/JS engines. I just could not personally find any info on type of limit or limitations. I think this is interesting discussion even beyond the voxel engine itself.

Maybe you have some personal experience with this or just know something that I don’t.

This is the extreme test:

As you can see there is about an area of 288*288 which is about 324 chunk meshes and each chunk mesh has a “flora” mesh for plants.
I separated “fluid” voxels into one mesh.

And it can actually render all of this. But at least for me it uses about 3.5 gb of ram.
It can run a bit slow when updating one chunk mesh.

But maybe this is un-reasonable thing to ask Babylon.Js to handle. Or could I do something to help it render a huge world?

One thing I was considering was combing all the chunk meshes into one mesh. But the entire world’s mesh would have to be re-built each time something is updated. I would have to store more data to quickly rebuild the entire world each time.

I do this with the fluid voxels but they are specials in the sense it should not be able to have as many exposed faces as the solid chunk meshes.

This could work in the case of games where you don’t need to break/destroy voxels. But does Babylon.js start to break down and get slow with ray casting with a mesh that may have 10mil+ vertices?

One thing I was thinking about to get over that was not actually using any form of ray casting or mesh picking. But simply in the world thread keep track of the player and where they area in the game space. From there I can get what voxel they are standing on and looking at and update the state accordingly. I could even do boundary checking to see if it is collide because I should know the bounding info for each voxel.

But maybe I am just pushing it to far and JS game engines can’t really handle a huge modern Minecraft like world.

You may find this of interest Minecraft Classic: A Truly OPEN Story | by Babylon.js | Medium

Have you explored any of the following?

1 Like

This seems a sensible idea.

Thanks for the reply JohnK.
But yes I have looked into the SPS and the documentation.
I have seen it suggested before that you should use a SPS.

I could see maybe using a SPS for each chunk mesh.
But I think that would only work in this case if I could have more control over the particles.
For instance I am not sure if it is possible to set custom uvs and colors on each particle like I do for each chunk mesh. I am not using a texture atlas. I am using a 2d texture array which is stitched together at runtime. It has to send a custom attribute to the shader in order to be textured correctly.

I think for voxel games though custom built meshes are the best way to go. At least with that system you have no more or less vertices than you need. Though with the SPS you get less draw calls but it is one huge draw call. And you may not even be using all the particles.

I actually started designing this voxel engine because I ran into a lot of problems trying to use a SPS to make an infinite generated game.

The SPS I think is really good for certain things. Like @nima voxel builder example.
But when pushed to its limit it begins to fall apart.

But maybe there is something about the SPS I don’t know. I think if I could use each particle like a chunk mesh and just update that particle that would work. But does each chunk mesh need to be a shape? Can I add to the added shapes vertices? How do I pass my custom uvs to each particle?

I am just being thorough in my Q/A in case anyone finds this threads and has asked the same questions as I have.

Hi nima,

Thanks for the input. I did not even know what is CSG is until right now. But I did not use that method.
Like I said the SPS is good for the kinda of stuff.

But your voxel editor is not exactly the same as large scale voxel world and it seems that the box particles themselves still have all their faces when connected.

I was designing a game last year that used a SPS and it was really hard to work with.

The voxel engine I wrote works and runs fine if the world is small enough. You could probably be good at like 15 - 20 chunk area render distance. But I am talking about getting it up to 32+ chunks render area. Which would be 1024+ chunks.

The method I used is “stupid simple” to render chunks. I have a 3d array of voxels stored in X/Z/Y order. It only has stored the Y’s that actually have voxels. It loops through the array and detects which voxels are exposed by checking all directions from it. From this it builds something called a chunk template. Which is the minimum amount of information needed to construct a visual representation of the data. This all happens in the “world” thread. The data is then set to a builder thread which takes all the data and interprets and expands it into full mesh data which is then sent to the main thread. This makes it easy to do things like flood fill lighting, ambient occlusion, connected textures, and more.

The only way I could really see a SPS working for this is if I could make each chunk mesh a particle. But that would be to actually change the geometry of each particle.

But like I said. Maybe I don’t know they best or most performant method. My case right here is pretty out there. The voxel engine works great for smaller scale worlds.

If anyone was curious I did find somewhat of a solution.
For voxel picking I was originally using facet data and the normals of each chunk mesh.
This was a lot of extra data and it had to be recalculated on every update. So instead I decided to move all voxel picking into the world thread.
Using the players position and direction I get a new point that is calculated by their reach distance.
From there with the two points you can use this function:

function visitAll(
 gx0: number,
 gy0: number,
 gz0: number,
 gx1: number,
 gy1: number,
 gz1: number
 // visitor: (x: number, y: number, z: number) => {}
) {
 const positons = [];
 var gx0idx = Math.floor(gx0);
 var gy0idx = Math.floor(gy0);
 var gz0idx = Math.floor(gz0);

 var gx1idx = Math.floor(gx1);
 var gy1idx = Math.floor(gy1);
 var gz1idx = Math.floor(gz1);

 var sx = gx1idx > gx0idx ? 1 : gx1idx < gx0idx ? -1 : 0;
 var sy = gy1idx > gy0idx ? 1 : gy1idx < gy0idx ? -1 : 0;
 var sz = gz1idx > gz0idx ? 1 : gz1idx < gz0idx ? -1 : 0;

 var gx = gx0idx;
 var gy = gy0idx;
 var gz = gz0idx;

 //Planes for each axis that we will next cross
 var gxp = gx0idx + (gx1idx > gx0idx ? 1 : 0);
 var gyp = gy0idx + (gy1idx > gy0idx ? 1 : 0);
 var gzp = gz0idx + (gz1idx > gz0idx ? 1 : 0);

 //Only used for multiplying up the error margins
 var vx = gx1 === gx0 ? 1 : gx1 - gx0;
 var vy = gy1 === gy0 ? 1 : gy1 - gy0;
 var vz = gz1 === gz0 ? 1 : gz1 - gz0;

 //Error is normalized to vx * vy * vz so we only have to multiply up
 var vxvy = vx * vy;
 var vxvz = vx * vz;
 var vyvz = vy * vz;

 //Error from the next plane accumulators, scaled up by vx*vy*vz
 // gx0 + vx * rx === gxp
 // vx * rx === gxp - gx0
 // rx === (gxp - gx0) / vx
 var errx = (gxp - gx0) * vyvz;
 var erry = (gyp - gy0) * vxvz;
 var errz = (gzp - gz0) * vxvy;

 var derrx = sx * vyvz;
 var derry = sy * vxvz;
 var derrz = sz * vxvy;

 do {
  //  visitor(gx, gy, gz);

  positons.push(gx, gy, gz);
  if (gx === gx1idx && gy === gy1idx && gz === gz1idx) break;

  //Which plane do we cross first?
  var xr = Math.abs(errx);
  var yr = Math.abs(erry);
  var zr = Math.abs(errz);

  if (sx !== 0 && (sy === 0 || xr < yr) && (sz === 0 || xr < zr)) {
   gx += sx;
   errx += derrx;
  } else if (sy !== 0 && (sz === 0 || yr < zr)) {
   gy += sy;
   erry += derry;
  } else if (sz !== 0) {
   gz += sz;
   errz += derrz;
  }
 } while (true);
 return positons;
}

Function was found here:

  • Edit : Replaced function with a better one. Works amazingly well.

This will give a list of discrete points from the reach point. From there you can start at index 3 to find the closest point and check to see if there is anything there.

Then when the player goes to actually add a voxel you can use a ray pick from babylon on a flat shaded cube that is in the voxels position to determine the normal which will give you the direction.

In terms of increasing the render distance I decided to aim for 225 chunks max with shader effects and massive terrain. While it could easily support 400+ without shader effects and more flat terrain.
Maybe in the future with WebGPU it will be able to handle more.

Seems that you may try also Levels of Detail (LOD) | Babylon.js Documentation for the most far meshes from camera.

1 Like

Hello!

I am using thin instances in my game to create a voxelized world. Here is a butchered version of a level (don’t want reveal the real stuff yet :smiley: :smiley: ) 440.000 cubes rendered at ~100 FPS. The GPU is not even sweating :muscle: and the memory usage is low.

EDIT:
This is running on a notebook GPU.

And the big brother (desktop GPU) can go even further (FPS capped by the refresh rate of my monitor):

I’m running on a quite good HW and I expect to achieve lower FPS on weaker HW, but I’m sure I can manage it to run 60FPS+ :vulcan_salute:

If you don’t want to draw your world using a shader I believe thin instances are the best option.

4 Likes

Thank everyone for the replies.

I think may all be valid ways depending on what you are trying to do and what exactly you are dealing with.

Though I guess for this case I am trying to create a voxel engine best for pixel art games. It can supported connected textures, different shapes, shading, and so on.

Here is screenshots of the “extreme test”.



I think one thing I could consider is if the game does not need to break/replace voxels I can combine all the chunk meshes into one.
The fluid mesh is combined into one so I can apply shader effects to it. I could apply that same method to all the voxel types to get a draw call of only five or so meshes for the whole world.

4 Likes

@nima One advice here. Maybe you are already aware of it, however:

If you are creating a lot of thin instances don’t use

mesh.thinInstanceAdd(matrix)

but create an array of matrices and call

mesh.thinInstanceAdd(matrices)

Or use thinInstanceSetBuffer.

It’s far more faster than adding them one by one.

:vulcan_salute:

3 Likes