Project Details

  • Weekend project
  • Our own engine
  • C++ and DirectX 11

Purpose

The purpose of this project was to learn more about the tessellation pipeline. I wanted to learn how to use different techniques such as PN-Triangles to smooth models and Displacement Mapping to create terrains.

Tessellation

When I first started working on this project, I wanted to learn about tessellation. So I started out by tessellating a subdivided icosahedron, this didn’t take too long since it’s quite easy to get simple tessellation up and running.

Once I got the tessellation working, I needed to find a way to make the mesh look smoother, otherwise the tessellation would’ve been pointless without wireframe turned on, unless you’re planning on displacement mapping. So, I found out about an algorithm called PN-Triangles which creates a bezier triangle for every triangle in the hull shader, and then calculates the new position and normal in the domain shader. As you can see in the image, the result of the PN-Triangles algorithm makes a huge difference, the technique used for this is described here.

Terrain

Once I had gotten a bit comfortable with the tessellation pipeline, I felt ready to move on with terrains, which is something I’ve been longing to experiment with.

I started out by creating a 20x20 grid and downloading a heightmap from a part of Mount Everest, using a cool tool that i found called terrain.party.

Now I had all the resources and knowledge that I needed to start making terrain.

Displacement

The first thing I did was implement displacement mapping on the terrain by sampling the height map using the UV coordinates of the current vertex. I then used this value to move the vertex in the y-axis.

Calculating Normals

Since I started out with a flat plane, all the normals were pointing upwards, so I needed to calculate new normals for the vertices.

This was accomplished using a Sobel Filter which is an edge detection filter, but it can be used to sample around a point on the height map and determine the normal direction in isolation, meaning it doesn’t have to know anything about the neighbouring vertices.

This is great, as we can do it for every vertex in the domain shader.

Adaptive Tessellation

One thing that is important to remember when working with terrains is the massive amount of polygons, this is taken care of using Adaptive Tessellation. What this means is that we tessellate more close to the camera, and less in the distance. When adding adaptive tessellation, it’s very easy to accidentally create holes in the mesh, I solved this using my own algorithm.

The Adaptive Tessellation Algorithm

We have to find a way to guarantee that a shared edge between two adjacent triangles has the exact same tessellation factor when both triangles pass through the hull shader. Since the hull shader is being passed 3 vertices, but is setting the tessellation factor on edges, we need to use 2 vertices to calculate the tessellation factor per edge.

Below is a very simplified version of my code that solves this, it is later tweaked to give a more smooth transition and also adds support for minimum and maximum values that tells it when to start and stop tessellating. This code takes place in the Patch Constant Function.

The result of this adaptive tessellation algorithm can be seen in the gif, when the camera moves closer to the terrain, you can see that it starts tessellating from the bottom of the image, as this is the closest to the camera.

It’s not very visible in wireframe, but this algorithm guarantees that there are no holes in the mesh, therefore it fulfills its purpose.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for(int i = 0; i < 3; ++i)
{
	float factor = 0.f;

	//This loop calculates the average tessellation factor for the 2 vertices that make up the edge
	for(int j = 2; j > 0; --j)
	{
		int vertexIndex = (i+j)%3;

		factor += distance(inputPatch[vertexIndex].myWorldPosition, cameraPosition) * 0.5f;
	}

	output.myEdges[i] = factor;
}

Texture Mapping

Next up is texture mapping, I used triplanar texture mapping for this, which you can find a lot about online.

Once I had calculated the triplanar UV coordinates, I could determine what texture should go where. As can be seen in the image, the color representations are as follows:

  • Cyan - Snow
  • Magenta - Grass
  • Yellow - Rock

First it checks how upright the surface is, to determine if it’s rock or a walkable surface. The way it distinguishes between snow and grass is by looking at the y-value of the world position.

This y-value could have added noise in the future to make it look more organic and not as gradient-like as it looks now.

Further Improvements

One major improvement that could be done to this terrain is to tessellate less if there are large flat areas or adjacent tiles that have similar inclination.

I could also add normal maps for the textures to add micro details, but for that, I’d have to calculate the tangent space for each vertex. It was outside the scope of this project, so I skipped it for now, but I might revisit this project at some point in the future since I had a lot of fun working on it!

Screenshot