Pass the tessellation, please! (Part 2)
Yeah, I know, the suspense was too much... :-) Who am I to torture you any longer? Here is the eagerly awaited next step in the tessellation pipeline - the evaluation shader.
After having defined the tessellation factors in the control shader, a fixed (non-programmable) tessellation stage takes over and creates all those little vertices and triangles for you. The result is then passed into the evaluation stage as patches whose size was defined in the control shader by the
layout (vertices = 3) out;
statement. Or rather, you're getting access to every single vertex of the patch (already existing and newly created) and the original vertex data at the same time. But we're getting ahead of ourselves, let's do this line by line.
Ouverture
Like the control stage, the evaluation shader starts with a layout definition statement:
layout (triangles, equal_spacing, ccw) in;
So we're expecting triangles to be passed in (the default). The next parameter can be a bit tricky: It defines how the triangles are distributed along the edges. There are three options:
-
equal_spacing: every segment of the subdivided edge has the same length. This only makes sense for integral tessellation factors, so any fractional factors are rounded up or down. This has the disadvantage that new vertices pop into existence when e.g. you're increasing the factor from 3.4 to 3.6, which might be noticeable.
-
fractional_odd_spacing: The tessellation factor is rounded down to the next lower odd integer (for example from 4.7 to 3). The edge is divided into (in this case) 3 equal-length segments and two additional smaller ones (taking up the space for the remainder of 1.7, each standing in for half of it).
-
fractional_even_spacing: Same as above, but the tessellation factor is rounded down to the next lower even integer (here from 4.7 to 4).
Sounds complicated, actually is a little complicated, but the simple rule is: In most cases where the tessellation factor is calculated (e.g. by a LOD formula) both fractional options give better results than the even one, because new vertices are created close to existing ones and smoothly glide along the edge with changing factors, giving less popping (you can see the effect in a video that I posted earlier on).
The final parameter in the layout statement defines in what order you would like your vertices to passed in, counterclockwise (ccw) or clockwise (cw). This has nothing to do with backface culling or anything, it just gives the order in the array data:
in ControlData { vec2 texcoord0; vec4 color; vec3 normal; vec3 binormal; vec3 tangent; } cData[]; out VertexData { vec2 texcoord0; vec4 color; vec3 normal; vec3 binormal; vec3 tangent; } vData;
So you're getting passed in the data for the original patch vertices as an array and have to produce corresponding data for every vertex in the patch. The evaluation shader is called once for every vertex (newly created or not). But where is its data? Well, you have to create it, which brings us into the
Main play
All information that the tessellation stage gives you for any vertex is its postiion - in barycentric coordinates. These are basically numbers between 0 and 1 defining the distance of the new vertex from the original corner vertices of the patch. And you use that information to interpolate all vertex data from the corresponding values of the corner vertices. Don't get your head spinning - it basically comes down to applying the same formula for every vertex attribute that you would like to pass along (the formula for quads is different, of course):
vData.texcoord0 = cData[0].texcoord0 * gl_TessCoord.x + cData[1].texcoord0 * gl_TessCoord.y + cData[2].texcoord0 * gl_TessCoord.z;
The built-in vector variable gl_TessCoord contains the mentioned barycentric coordinates. As you can see, you basically multiply the vertex attribute for every corner vertex with the corresponding barycentric coordinate to get the value for the newly minted one - in this case for the first set of UV coordinates, but the same formula applies to color, normal etc. Only the position uses an additional built-in variable
vec4 vPos = (gl_TessCoord.x * gl_in[0].gl_Position) + (gl_TessCoord.y * gl_in[1].gl_Position) + (gl_TessCoord.z * gl_in[2].gl_Position);
As you probably have guessed, gl_in contains the original vertices and the field gl_Position their position. And last but least, here again you have to export the vertex position in the special variable gl_Position:
gl_Position = projectioncameramatrix * vPos;
If you remember, I removed the projection of vertices from world into camera space from the vertex shader, so that we can later calculate tessellation factors using world space data. But at some point before the fragment shader we have to apply this projection, and this is actually the last chance to do that...
Finale
well, nothing left to say, the pass-through tessellation shader is done! I have attached a small zip file, if you extract that into a project folder you will have
- the PassThrough.shader (in Shaders->Model)
- a PassThrough.mat (in Materials->Developer)
- a simple triangle.map (well, in Maps...) containing a box with the shader applied.
The material displays the box in wireframe mode so you can see the effect (by the way, you have to run the game to see that, it won't be shown in-editor). Since a pass-through isn't too exciting, you might want to experiment a little with the shader: modify the tessellation factors (gl_TessLevelInner and gl_TessLevelOuter in the Control shader) and the edge subdivision mode (equal, odd, even) in the Evaluation shader.
What's next?
Those manual factors aren't of great help, so we'll dynamically calculate the tessellation based on camera distance and other factors. Hope to see you again!
- 6
0 Comments
Recommended Comments
There are no comments to display.