This picture shows the final result of a 2-semester long project to construct an underwater scene in C/OpenGL. It took a
lot of work to get to that point as almost everything had to be written from scratch. There are no fancy libraries or
game engines, just pure pain. Before I get too far into this, this is not some highly original concept. I was inspired
to recreate this scene from the video below. I’ll also be borrowing some ideas to make this all work.
Where to start?
It’s hard to figure out how to get going on a project like this. What should come first, the fish or the terrain? I
decided on starting with Boids and pushing the nightmare that is terrain generation onto the backlog.
Boid’s algorithm isn’t really a tough egg to crack. For those that are unfamiliar, it’s just 3 simple rules:
Separation - Boids want to be close to each other, but not too close, apply a strong repulsive force if boids get too
close to one another.
Alignment - Try to point in the same direction as other boids around you.
Cohesion - The opposite to separation, move towards the centroid of nearby boids. This causes boids to want to stay
together.
That’s really it, for now. We’ll come back to this later.
Super Simple World Generation
Okay, I may have lied; this part was sheer pain. Anyway, let us get into it. Inspired by the video linked above, I
wanted to generate a world with the Marching Cubes
algorithm - Marching Tetrahedra struck me as a bit intimidating.
What is a marched cube?
The marching cubes algorithm explains how to generate a set of triangles to approximate a scalar vector field. More
simply, if you have points in a 3d space, this is how you generate triangles to approximate a ‘surface’ that those
points would create. Don’t worry too much if my 2-sentence crash course didn’t do the trick, there’s going to be some
helpful graphics below.
Generating World-like Scalar Vector Fields
Marching cubes is not something that will generate a world in isolation. There has to be some sort of input. Marching
cubes take in a function and outputs a set of triangles. So what should that function be? Well, the good news is that I
don’t have to solve this problem. Smarter people have come along and tackled this a long time
ago. Perlin noise is a multidimensional function which when paired with
marching cubes produces life-like terrain. Again, we’ll visualize this process below, keep going!
It should be noted that I’ll use Simplex & Perlin Noize interchangeably here. For all intents and purposes, they’re the
same thing, Simplex is much faster.
The basic approach
Here’s the 4-step plan to get the world generation working:
In hindsight, it was not that easy.
First Try
The above shows my first crack at my 4-step plan. There are also a few boids in there - because why not. It may be
apparent that this is pretty terrible. Firstly, we need some colors. This could be done in so many different ways, the
approach I went with is using the noise function to generate a lower frequency function mapped to a color. So when I
assign a noise value to a point, I also generate a color value.
Next, the size of the world has to be a lot larger. I had a small world initially because the program was terribly
unoptimized. The problem is that I have an awesome GPU capable of handling these complex calculations in parallel but
instead chose to do each calculation sequentially on the CPU. For prototyping this was fine, but it was time to get into
the nasty stuff.
Shaders
Before, all the work for drawing the world was being done on the CPU. This is only okay for small amounts of work. In my
case, I wanted to recreate the entire world every frame so the CPU was not going to cut it. While a CPU has maybe 8
cores, a GPU has many, many, more cores. This means that parallel workloads can be completed much faster. Moving the
relevant calculations over to the GPU is not a trivial task.
Firstly, a VBO is assigned. This is a block of memory reserved in the GPU for our calculations. Ideally, we will write
to this memory space with our input data, kick off the calculations, and ask for the GPU to write the resultant
triangles
to the active frame buffer. I could spend a long time explaining my horror stories of this process, but it’s easier to
show you instead:
How did I fix it? No idea. But my hard work did pay off. Now the world is a bit larger and it has colors!
Back to Boids
So you may have noticed that the boids disappeared in the last video. They have not been forgotten about. A lot of work
is needed to integrate them into the shader hellscape I have just created. Firstly, we need them to spawn around the
player. Since the world is infinite, we cannot spawn a set amount of fish and call it a day. To accomplish this, I broke
the world up into 9 chunks relative to the player. When a chunk is too far, it gets deleted. When a new one is
created, it’s populated with a bunch of new boids.
Terrain Collision
When the boids move throughout the world, they better not phase through anything. If this were Unity, we’d throw on a
box collider and call it a day. OpenGL gives you nothing of that sort. So collision detection and avoidance will have to
be written entirely from scratch.
Firstly, if we’re a boid, how do we know if we’re about to hit something? If you said anything to do with sight, you’re
completely wrong. As written, this program does not store the state of the world. It’s regenerated every frame inside
the GPU. The game itself does not know about the definition of the world.
Instead, we have to perform a voxel-raycast outwards from the perspective of the boid, generate the world geometry from
the voxel raycast, and perform ray-triangle intersections with the generated geometry and the boid’s velocity vector.
Voxel Raycasting
For those unfamiliar, a raycast is the process of shooting a ray and performing operations at different points along
that ray. For this, I used A Fast Voxel Traversal Algorithm for Ray Tracing.
This is a quick visualization I made of a blue ray intersecting 4 red voxels (interactive):