I'm DIY'ing It - DevLog
I went on a bit of a deep dive recently to find different methods to achieve the same, or better, outcome to various problems that would stretch my abilities a little bit. During this time I watched a lot of dev logs on YouTube and read a lot of blogs for any kind of inspiration or interesting content (as unfortunate as it sounds, programming is really difficult to make entertaining). Eventually I came around to the channel of Sebastian Lague who’s recent uploads are all small 10 minute summaries of concepts that basically everybody finds interesting, one of these was a video on using marching cubes to generate voxel terrain. Apparently this is a technique that everyone uses and I’ve just never heard of it so I’ve started working on it.
The first thing I did was, stupidly, not look into the concept any further than that single video and instead find a tutorial and go straight to the deep end. Eyes closed, head first, can’t lose. This was a mistake. The tutorial I found was actually super helpful, it gave me a lot of initial progress and understanding and I managed to get a nice 2D representation working. It wasn’t pretty, it definitely wasn’t optimised, but I was getting it.
The first part of the tutorial says that they won’t go into cubes and instead stick with the process in 2D as it would be a bit much, I naively thought an extra dimension couldn’t be that difficult to implement and went straight for it, thinking I could add a few extra for loops and everything would be fine. And honestly it started out like that, using a single matrix to create a bunch of voxels with randomly assigned values and joining them together to make a single mesh. Then I tried to implement an extra chunk, not only did the split between them not join together as the 2D version did, some of the points weren’t actually being placed in the correct position.
Instead of fixing these issues I commented over some bits of code to go back to a semi-working version and try and interpolate the positions based on the values of the points. Unfortunately this wasn’t working either, I hadn’t really been working from a ‘surface level’ from the start and couldn’t get the values to behave correctly.
Instead of giving up here like I thought I was going to, I took a day off from everything and came back with a new approach. I found some resources and tried it for myself instead of following a tutorial.
This new approach would still use a few areas of the tutorial versions, mainly the general approach I’d use, but instead I started this version with a single cube. By creating this version it gave me a better idea of what the different shapes would look like in a complete version, it also gave me a good starting point for what my matrix would actually be filled with, references to points that have an array of corner points. While I could simply reference the corners themselves this felt like an easier way for me to get certain references and keep everything tidy (I’ll arrive at specifics soon).
From this single cube test I expanded it into a full class with references to its corners (as their own class), and the positions of the edges; I put these into a 2D array to create a basic grid. Something from the initial video I liked was the closed off edges, instead of having no mesh and letting the user see under the ‘terrain’, capping off the edges instead. To achieve this I checked if the current point was on an edge and set the correct corner values to below the surface. This gave a nice cap to the outside edge of the grid and made the whole thing look a little bit neater.
After making sure the cubes were generating correctly I started implementing chunk support, this was something that I felt was going to be really important if it was going to be included in any real project since performance was being a bit sluggish at higher surface resolutions. The initial problem was the same as the original tutorial, there were very obvious gaps between any two chunks that didn’t create any kind of mesh.
This was actually a fairly easy fix using similar methods from before, by giving each chunk a set of references to the previous chunk in a given direction we can get the values of that chunks points. If the current chunk doesn’t have a neighbor in a certain direction then it’s the first one of that row, meaning we can close off the edges if we want to (which looks way better so we do). When we come to the next chunk we’ll set the shared points to the values of the original and create a clean transition.
It's worth noting that everything written previously was done after I made it. We're now writing live. Which means there's a lot of talking about my improvising.
With that information, blending the seams just took 4 hours. I thought I'd already successfully implemented it and it turns out that was incorrect. So scratch some of that because it works a bit differently. We get the previous chunk in a given direction for the neighbor and change the capped values to share the open values of the new chunk. We’ve also pushed back meshing any of the chunks until all of them have been triangulated properly, just to make sure we don’t get any visual errors while all of the final values are technically correct. Definitely wasn’t what an hour of that work was…
One of the main issues we had at this point was that the corner values weren't being set properly. Because of the way we're copying values between corners we end up with these huge gaps between certain edges of chunks. All this really needed was to check it against certain neighbors of neighbors and the values get brought through totally fine.
So our next step is to make it look more like terrain, meaning it needs to start as a flat plane. Right now we’re checking the value of a corner against a surface level, with random values it’s limited from 0 - 1. So if we make every corner simply be the negative of its height in the world we’ll get a point where the corners flatten out at 0, the surface level.
However, if we make the surface level too low a second layer of the terrain appears on the lower levels of the matrix, this should be resolved by making sure we check the actual world position of the corners and not their local position. We’ll try passing in the world position to the corners as we create them instead of just setting the local position and see how it turns out.
And that totally worked, we pass through the localPosition of the matrix (the center of the matrix) and add that to the local position of the point we’re making. Then while initialising the corners we do the same thing. If we just pass through the world position straight through to the corners there are enormous gaps between the chunks.
So now that we’re making a real plane for the base of the terrain we can start affecting it with noise and other factors. Here’s a good example of when we add some noise to that same surface.
It’s at this point I’m tempted to start moving this into a compute shader for optimisation, because right now this is basically the biggest space we can make and the highest resolution available. While in-game performance is totally fine (because we’re not generating the chunks in real time, we’re just creating them before actually loading into the world) the performance when we’re calculating the terrain is taking ages.
The screenshot to the left is 8^3 size over 3^3 chunks at 32^3 resolution which looks really detailed, awesome, but it took just over 3 minutes to generate which isn’t great. So before we move forward on the actual terrain generation we’ll start looking more at compute shaders that can recreate this.
That's gonna definitely be starting tomorrow. I'm happy with the outcome of this one but we'll definitely need a more streamlined method before making anything more detailed.