There are several key changes in the latest update to Voxtric to be discussed starting with the new way the rendering blocks is handled. Instead of using a texture atlas as I had been before and storing the texture position of each block in an array to be accessed by block index, I now use texture arrays. This means I still only need to to one texture binding and only use one texture per mesh, but the way it is accessed is now completely different. It is literally like having an array of textures that can be used. I don’t have to store a texture position at all, and instead just use the block ID of a voxel to access the right index of the texture array which the fragment shader can then sample.
There are several benefits to handling my rendering of meshes this way compared against using a texture atlas. The first major one is that it now means I can have individual images for each texture that can be named specifically for the block that they represent. The second major in game benefit is that texel bleeding as can be seen in my previous videos of Voxtric is no longer an issue. The pixels in the texture surrounding the block the fragment shader needs to sample will not be taken into account at different mipmap levels.
Another key change that I have made to rendering is to implement frustum culling. There is no doubt that this is an absolutely essential part of any game engine, but initially I had not done it as my test scenes didn’t need it to run. Now that I have a better grasp of the Bullet physics engine and the key underlying structure of what I am trying to accomplish with Voxtric is in, I decided now was the time to go back and re-do the rendering pipe-line.
The final important point that has changed is that the centre of mass and total mass of the RegionCollections are no longer broken. Each block is defined in a .csv file containing its ID, name, if it’s visible, if it’s solid, and of course its mass. When each block is checked in the voxel data for building a mesh, it’s position compared to the middle position the data is multiplied by it’s mass which is then added to a total position.
At the end of the mesh generation the total is divided by the number of voxels stored. This position is then considered to be the centre of mass. The average of all the different mesh position centres is then used to decide the RegionCollection centre of mass. As can be seen in the video above, it is a massive improvement when compared to my previous videos.
This post and the video above are short due to the single change that took place in Voxtric for this post to be made, but the change itself is a big one. After a fairly large amount of work I managed to get algorithm for checking for and splitting off of disconnected collections of voxel data.
Method (Has since been far and away improved)
It works by storing a series of points around each block that is removed at once (6 for each block removed), and then checking each point for a solid block at that positing, throwing the point away if there is no solid block at the position. Doing it this way means that large areas of blocks are able to be removed at once and the rest of the algorithm as is about to be explained will still work.
A DataSplitFinder is then created at each point which stores a std::vector of positions found, an std::map of the positions found in the most recent round of checks (explained in a moment), and a std::vector of the positions found in the previous round of checks. It also stores the total number of positions found.
Each DataSplitFinder then performs a breadth first search for the other finders around it. When one is encountered, they are merged together and the process continues. If a finder cannot continue its breadth first search as there is nowhere else for it to search, it means that this finder has detected a collection of voxel data that is no long connected to the main body of voxel data. A new RegionCollection is then created and the voxel data that has been identified as not connected is copied over to the new RegionCollection. This process continues until there is only one finder left as all active finders will have been merged together of removed when copying data.
As it is faster to copy the least data possible from a large voxel data collection, the DataSplitFinder with the least voxel positions found is the one that performs each round of its search more often than the others. This is because, in the case that a split has occurred, in places where the finders can reach each other they will quickly merge into one finder and each round will spread over a much larger area. The smaller section though will have the constraints of being on a boundary of voxel data and will have merged with fewer if any finders.
As this smaller section is the bit we want to copy out, it is the one we perform the breadth first search operation on until it is either confirmed to show a split in the voxel data, it finally meets up with the other finders, or the number of positions it holds becomes more than that of any one of the other active finders. In the latter case, the exact same process is repeated, just with whatever finder now has the least voxel data positions stored.