Voxel terrain generation with Perlin noise and instanced rendering

The goal of this project was to achieve a Minecraft like terrain generation. I was especially curious about any performance pitfalls that might arise. I used my own engine, because I wanted to test and possibly extend its capabilities.

The first thing I needed was the option to apply different textures to a cube’s faces, so I can display a proper grass block. I did this by adding a third dimension to the texture coordinates, where the value is the same for all fragments that belong to the same face. The value is treated as an ID, that corresponds to a certain texture.

#version 330 core

uniform sampler2D texture0;
uniform sampler2D texture1;
uniform sampler2D texture2;

in vec2 texCoord;
flat in float texID;

out vec4 FragColor;

void main()
{
	vec4 color;

	if (texID == 0.0)
		color = texture(texture0, texCoord);
	else if (texID == 1.0)
		color = texture(texture1, texCoord);
	else if (texID == 2.0)
		color = texture(texture2, texCoord);

	FragColor = color;
}

After some research, I learned that Perlin noise is the usual way to generate procedural terrain with smooth gradients. When I implemented the Perlin noise function and used its output to draw a 256×256 grid of cubes, I immediately noticed serious z-fighting. At a certain distance, cube faces were overlapping with their neighbors.

Z-fighting

I found that this was caused by near clipping distance being set too low in relation to far clipping distance. The depth buffer has higher precision the closer it is to the camera, so when near clipping distance is very small, precision starts to suffer already at perceived medium distance.

Another problem was the framerate. Despite the game doing nothing but rendering 256×256 cubes, it only ran at 20-30 frames per second. The reason is that I was issuing a draw call for each of the 65536 cubes, every frame. This caused a lot of expensive communication between CPU and GPU. The solution is a technique called instanced rendering, where all the different cube positions are transfered at once, using an array buffer. This way only one draw call is needed and OpenGL instructs the GPU to iterate through the positions and I only need to add them as an offset in the vertex shader.

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aTexCoord;
layout (location = 2) in vec3 aOffset;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec2 texCoord;
flat out float texID;

void main()
{
   gl_Position = projection * view * model * vec4(aPos + aOffset, 1.0);
   texCoord = aTexCoord.xy;
   texID = aTexCoord.z;
}

As a final note, the Perlin noise function also causes quite a lot of performance overhead, when calculating this many positions at once. So whenever terrain needs to be generated in realtime, one should generate considerably smaller chunks per frame.

Feel free to explore the source code of this project as part of my engine’s Github repository.

Latest Posts

Categories

Archives

WordPress Cookie Plugin by Real Cookie Banner