In the last OpenGL SDK post I introduced the new SDK, now let me introduce our first new sample, “Simple Tessellation Shader”. As the name implies, it demonstrates the minimum parts necessary to use tessellation shaders in OpenGL. The sample only demonstrates tessellating the quad domain (rather than the triangular domain), and it shows two modes: one with a flat plane and one where the plane is wrapped into a torus.

About Tessellation Shading

OpenGL recently added the concept of tessellation shading to its definition of the graphics pipeline. It expands the pipeline by two additional programmable stages and a single fixed-function stage that handle the specification and evaluation of a tessellated surface. It all fits into the pipeline like the diagram below.

These new tessellation stages operate between the vertex and geometry shader stages previously available in the pipeline. The initial stage is the known as the “tessellation control shader” (TCS). This shader accepts a list of vertices defining a patch from the vertex shader and controls the amount of tessellation applied to the patch. The next tessellation stage is the fixed-function tessellator. The tessellator takes the amount of tessellation provided by the TCS and computes a pattern of triangles in a parametric space. (For our example today, the space is a quadrilateral with u and v directions.) Finally, the “tessellation evaluation shader” (TES) is executed at least once for each vertex that was created in the parametric space. The TES takes the parametric coordinate and the patch data output by the TCS to generate a final position for the surface.

Diving in a bit deeper, the TCS is one of the more complex shader stages in the pipeline due to all of the options it controls. The TCS specifies the number of vertices it creates in a patch via a layout statement. (e.g. layout (vertices=1) out ) In the TCS, each output will have its own thread to generate the output vertices. One thing that makes the TCS special, is that the threads can all see all of the input data, and at the end of the shader, they can share their results to allow group computations for items like patch-wide tessellation levels. (These are more advanced features that we’ll skip in this sample)

Moving on the tessellator, it is very much a black-box from the point of view of the graphics programmer. All it really does is generate a sequence of u.v coordinates and an associated topology map to control how the patch is converted to triangles. While the amount of tessellation is controlled by data output by the TCS, the TES declares an input which controls the pattern of triangles generated. Once the points are generated, the tessellator launches one thread per point to the TES.


Finally, the TES evaluates and transforms points into clip space to define the surface. It has as input the output from the TCS defining the patch as a whole, and a u,v coordinate from the tessellator defining where on the surface this particular point should lie. In many ways, it operates much like a vertex shader with an additional set of data that is uniform per patch rather than uniform for all patches.

Sample Shader Overview

The first shader to consider in our sample today is the vertex shader. It has been descriptively named passthrough_vertex.glsl, as it does no real work.

 layout(location=0) in vec4 vertex;    out gl_PerVertex {      vec4  gl_Position;  };    void main() {      gl_Position = vertex;  }

The vertex shader simply declares a single input that defines the location of the patch, and it copies it through to the next shader stage. Had this been a more complex example, work such as animation transforms (skinning for skeletal animation) would be best applied in the vertex shader, because it would create a final object-space position. Additionally, since patches likely share adjacent vertices, just like triangles, performing shared work here can reduce redundant computations.

The TCS shader in this example is not much more complex than the vertex shader. Because the sample just tessellates to a user-controlled constant, there are no real computations to be performed at the patch level.
 layout( vertices=1) out;    out gl_PerVertex {      vec4  gl_Position;  } gl_out[];    #include '/uniforms.h'    void main() {      gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;        gl_TessLevelOuter[0] = outerTessFactor;      gl_TessLevelOuter[1] = outerTessFactor;      gl_TessLevelOuter[2] = outerTessFactor;      gl_TessLevelOuter[3] = outerTessFactor;        gl_TessLevelInner[0] = innerTessFactor;      gl_TessLevelInner[1] = innerTessFactor;  }
The big thing for users unfamiliar with tessellation shaders to notice is the introduction of a whole bunch of new built-in variables. The gl_TessLevelOuter built-in controls the tessellation level for the four outer edges of the patch. In the diagram below, OL0 corresponds to element 0 of the array of out edge tessellation factors. Similarly, the gl_TessLevelInner[0] corresponds to IL0 marking and represents the interior divisions in the u parameter space.
(0,1)   OL3   (1,1)
+--------------+    
|              |  
|  +--------+  |  
|  |   IL0  |  |  
OL0|  |IL1     |  |OL2 
|  |        |  |                    
|  +--------+  |    
|              |  
+--------------+  
(0,0)   OL1   (1,0)

Now that the TCS has setup how finely to tessellate the patch, the TES is where all the real interesting work occurs.

 layout(quads,equal_spacing,ccw) in;    out vec3 normal;  out vec3 vertex;    out gl_PerVertex {      vec4  gl_Position;  };    #include '/uniforms.h'    void main(){        vec4 position = gl_in[0].gl_Position;        position.xy += gl_TessCoord.xy * 2.0 - 1.0;        normal = vec3(0.0,0.0,1.0);        gl_Position = ModelViewProjection * position;        normal = (ModelView * vec4(normal,0.0)).xyz;      vertex = vec3(ModelView * position);  }
The first thing to notice in the TES is the input layout statement. It defines that this TES expects to be operating on a quadrilateral grid parameterized by u and v, that the tessellation subdivisions will happen at equal steps, and that the resulting triangles should be ordered in the standard OpenGL winding of counter-clockwise. The input array gl_in holds the array of patch parameters. Since our TCS only had a single parameter defining the center of the patch, this is the only data it contains. The built-in gl_TessCoord contains the u,v parameters of where on the patch this point belongs. This simple plane shader is just expanding in the x and y dimensions from [-1.0, -1.0] to [1.0, 1.0]. As a result, the math required to modify the parametric u,v coordinates into offsets is clearly very simple. Finally, the position and an outward facing normal are transformed to produce both eye-space and projection-space values.

A more complex version of the TES is also included in the sample. This version uses sin and cos to wrap he u,v space around to form a torus. It also demonstrates how you may need to perform additional math to compute proper normal for the surface generated. (In the case of the torus, it is just more sin and cos calls)

Beyond Shaders

Outside the tessellation shaders there are a couple other bits of the sample worth noting. First is the call to glPatchParameteri. This is what sets the number of input vertices per invocation of the TCS. Since the sample is using one point to define a patch, the value is set to 1. Next, rather than having all shader stages linked into a single shader program. The shaders are compiled as separate shader objects and bound into a shader pipeline. This allows switching the torus TES in and out with the plane TES without changing any of the others. It offers a moderate amount of convenience, especially for those used to a DirectX-centric pipeline. Finally, some readers might have noticed that my shaders were using user-defined uniforms, with no matching declarations in the shader code above. This is because the uniforms were all centralized into a single uniform buffer object defined in an include file. Using the recent GL_ARB_shading_language_include, the shaders were all able to share the same definitions. Additionally, with a bit of macro and typing work, the same include defines a struct with the necessary alignments to match the uniform buffer. With this, the uniform buffer update is as a single copy with glBufferSubdata from the program struct.

Conclusion

While this is clearly a very simple sample, it should get those interested in learning tessellation shaders with OpenGL a start. Also, this sample gives us a chance to ease into the new SDK. We’ll be back soon with more introductory material for new features like this, as well as some material to dive deeper into what you can do with OpenGL today.

Notes

  • You can download the sample code here
  • If you have questions or comments about the sample, please discuss it on this DevTalk thread. Thanks!