Tessellated Terrain


In OpenGL 4.x there are 2 new programmable stages of the pipeline used to do tessellation. This allows for an efficient way to generate lots of triangles on the gpu in a shader instead of loading them in through a vertex buffer. You can also dynamically choose the number of triangles to be generated based off some input parameters. This could be used to efficiently smooth out lower res polygonal models by generating new triangles on their triangle faces and using a bezier/bspline surface equations to position them. Importantly since you can dynamically choose the number of polygons to generate you can implement level of detail and use more polygons for things closer to the eye. In this tutorial I’ll show how to use the same concept to easily draw terrain where the parts closer to the eye have more polygons and farther away have less. Parts completely out of the view frustrum will even be culled. This can be seen in the above picture (full screen it to see better). The terrain farther away in the distance is drawn with fewer polygons.

OpenGL setup:
1) Load heightmap and its normals into textures
2) Create a flat grid made of quads in the region 0,0 -> 1,1 (this is done so that the local coordinates of the quads can be used as texture coordinates into the height map). The size of these quads in the grid will be the minimum detail possible that your terrain can be drawn at. Just use something reasonable like .01x.01 for now and you can change it through experimentation later.
3) call glPatchParameteri(GL_PATCH_VERTICES, 4); since we will be passing in quads
4) Draw the grid using glDrawElements(GL_PATCHES,…) with following shaders.

Vertex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#version 410 core
layout (location = 0) in vec2 vPos;
 
layout(std140) uniform Object {
	mat4 worldTransform;
	mat4 normalTransform;
};
 
uniform sampler2D heightMap;
 
out vec3 tcWorldPos;
out vec2 tcTexCoords;
 
void main()
{
	tcTexCoords = vPos.xy;
	float y = texture(heightMap, tcTexCoords.yx).x;
	tcWorldPos = (worldTransform * vec4(vec3(vPos.x,y,vPos.y), 1)).xyz;
}

As you can see this is no ordinary vertex shader. Instead of projecting vertices to the screen I am just computing their world position and texture coordinates to be used by the next shader.

Tessellation Control

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#version 410 core
#define ID gl_InvocationID
 
//generate uv coordinates for vertices in output triangles.
layout (vertices = 4) out;
 
uniform vec3 eyePos;//the distance of this to the worldPos determines the number of triangles to generate
uniform float lod=.1;
 
//inputs from vertex shader
in vec3 tcWorldPos[];
in vec2 tcTexCoords[];
 
//pass local/texture coordinate through unchanged
out vec2 teTexCoords[];
 
//convert worldPos to normalized device coordinates
vec3 ndc(vec3 world){
	vec4 v = eye * vec4(world,1);
	v /= v.w;
	return v.xyz;
}
 
//determine if is vertex is on the screen.  This is used to do culling.  My assumption is that if all vertices are off the screen then the patch should be discarded.
//Unfortunately this isn't true if you stand close to a patch and look at its center.  The extra stuff you see is an attempt to prevent culling of a patch if it is close to the eye.
bool offScreen(vec3 vertex){//vertex should be ndc
	vertex = ndc(vertex);
	float z = vertex.z * .5 + .5;
 
	float w = 1 + (1-z) * 100;
	return vertex.z < -1 || vertex.z > 1 || any(lessThan(vertex.xy, vec2(-w)) || greaterThan(vertex.xy, vec2(w)));
}
 
//make a custom func to determine a tessellation level based on distance of the patch from the eye
float level(float d){
	return clamp(lod * 2000/d, 1, 64);
}
 
void main()
{
	//pass local/texture coordinate through unchanged
	teTexCoords[ID] = tcTexCoords[ID];
 
	//for some reason this shader is called once for every vertex in a patch even though the tess level only needs set once.  For efficiency only calculate it once.
	if (ID == 0){
		if (offScreen(tcWorldPos[0])
			&& offScreen(tcWorldPos[1])
			&& offScreen(tcWorldPos[2])
			&& offScreen(tcWorldPos[3])){
 
			//all patch vertices off the screen, cull the patch
			gl_TessLevelOuter[0] = 0;
			gl_TessLevelOuter[1] = 0;
			gl_TessLevelOuter[2] = 0;
			gl_TessLevelOuter[3] = 0;
 
			gl_TessLevelInner[0] = 0;
			gl_TessLevelInner[1] = 0;
 
			return;
		}
 
		//Calc tessellation based on distance from eye
		float d0 = distance(eyePos, tcWorldPos[0]);
		float d1 = distance(eyePos, tcWorldPos[1]);
		float d2 = distance(eyePos, tcWorldPos[2]);
		float d3 = distance(eyePos, tcWorldPos[3]);
 
		//specify tessellation on edges of the quad, make tessellation identical on bordering edges of quads or there will be gaps.
		//left
		gl_TessLevelOuter[0] = level(mix(d3,d0,.5));
		//bottom
		gl_TessLevelOuter[1] = level(mix(d0,d1,.5));
		//right
		gl_TessLevelOuter[2] = level(mix(d1,d2,.5));
		//top
		gl_TessLevelOuter[3] = level(mix(d2,d3,.5));
 
		//Inner value doesn't really matter, just make it something similar to Outer
		float l = max(max(gl_TessLevelOuter[0],gl_TessLevelOuter[1]),max(gl_TessLevelOuter[2],gl_TessLevelOuter[3]));
		gl_TessLevelInner[0] = l;
		gl_TessLevelInner[1] = l;
	}
}

This new shader servers one primary purpose. It uses its inputs from uniforms and the vertex shader to determine the number of triangles to generate. This is done by setting gl_TessLevelInner & gl_TessLevelOuter. See here for more info. The output will be the new triangles vertex coordinates. Coordinates can be barycentric coordinates for triangles (layout (vertices = 3) out;) or uv coordinates for quads (layout (vertices = 4) out;). This example uses uv coordinates since our patch is a quad. A uv coordinate is a vec2(x,y) between 0-1 specifying the position of the vertex inside the quad. This confused me a lot at first, It will make more sense after seeing the next section.

Tessellation Evaluation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#version 410 core
 
struct Light {
	mat4 eye;
	vec3 pos;
	vec3 color;
	vec4 pad[2];
};
 
layout(std140) uniform Global {
	mat4 eye;
	vec3 eyePos;
	int numLights;
	Light lights[6];
	float time;
	float deltaTime;
	vec2 screenDim;
};
 
layout(std140) uniform Object {
	mat4 worldTransform;
	mat4 normalTransform;
};
 
layout(quads, equal_spacing, ccw) in;
 
in vec2 teTexCoords[];
 
//output localPos and worldPos for shading
out vec3 gWorldPos;
out vec3 gLocalPos;
 
uniform sampler2D heightMap;
 
//bilinear interpolation
vec2 interpolate(vec2 bl, vec2 br, vec2 tr, vec2 tl){
	float u = gl_TessCoord.x;
	float v = gl_TessCoord.y;
 
	vec2 b = mix(bl,br,u);
	vec2 t = mix(tl,tr,u);
	return mix(b,t,v);
} 
 
void main()
{
	//generate vertices by interpolating patch vertices and tessellation coordinates
	vec2 texCoords = interpolate(teTexCoords[0], teTexCoords[1], teTexCoords[2], teTexCoords[3]);
 
	//lookup terrain height from height map
	float y = texture(heightMap, texCoords.yx).x;
	gLocalPos = vec3(texCoords.x,y,texCoords.y);
 
	gWorldPos = (worldTransform * vec4(gLocalPos, 1)).xyz;
	//finally project vertices to the screen
	gl_Position = eye * vec4(gWorldPos, 1.0);
}

As previously mentioned the input to this shader is a uv or barycentric coordinates of a vertex in a newly generated triangle. To convert this coordinate to actually be a vertex position we use interpolation from the 4 local space verts == texture coords (tcTexCoords) we have been passing through. With these texture coords we can look up the height to get the local space y. Finally we project the vertex like we would normally do in the vertex shader.

Geometry/Frag

Shade as desired

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>