Go to OpenGL Home
We continue the first chapter of this tutorial series by looking into some back-end operations over which we don’t have direct control.
Up until now, we have discussed vertex and fragment shaders (geometry and tessellation are reserve for later chapters), but between the vertex and fragment parts of the pipeline, there are a series of operations which make the transition from vertices to fragments, the pipeline backend.
The first step in this process is the primitive assembly, which consists of grouping the received vertices into the primitive type set in the main program (triangles, lines or points – the trivial case).
The vertices exiting the vertex shader are all in clip space, represented as quadruplets (vec4), with the first 3 establishing their coordinates in Cartesian space and the 4th representing the w value, together making up a homogenous coordinate. The w value is used in the perspective division operation, through which homogenous coordinates are transformed into Cartesian coordinates by dividing the first 3 values by w.
As a result, the new position is in normalized device space (x and y between -1.0 – 1.0 and z between 0 -1). If the resulting values are included within the screen values, then the vertices will be represented, otherwise they will be dropped.
Before the above operation, the actual clipping takes place. Let’s assume that a primitive is only partially present inside the clip space. Instead of dropping it altogether, the clipping operation splits it so as the parts that are inside the clip space will be their own primitives, which will be drawn, while the others will be dropped.
The culling operation further eliminates primitives based on its orientation to the observer. Thus, each triangle has a front and back side. If the front side is facing the viewer, it will be drawn, otherwise it will be dropped.
To determine this, OpenGL uses the following formula:
Here, x and y are the window space coordinates of the i-th and (i+1)-th vertices from a certain triangle. If a is positive, it’s front facing. The formula is a sum of 2 2X2 determinants, which will result in the sign of the triangle. Of course, this operation can be reversed by changing the order in which the vertices are transmitted (or the winding order):
The default in OpenGL is the counter-clockwise one (the left one). This can be changed by using the function glFrontFace() with one of the 2 parameters (GL_CW, GL_CCW). To turn on culling, you call glEnable(GL_CULL_FACE) at the start of your program.
If the front side is facing the viewer it will be drawn, otherwise the back facing one(s) will be culled. For opaque objects this gives roughly a ~ 50% performance increase. For rendering transparent objects you typically render all the back-facing triangles first, then render all the front-facing triangles.
Rasterization is the last operation before the fragment shader and it consists of determining which fragments might be associated with a primitive or line. Although there are many approaches to this operation, the main one used by OpenGL is based on a half-space-based method for triangles.
It encapsulates the triangle in a boundary box in window coordinates, it treats every edge as a separator, dividing the window in 2 and checks every fragment to see if it is inside the triangle. This is a simple algorithm which lends itself to massive parallelization.
The last operation we shall touch upon is one that follows the fragment shader, coloring the fragments according to the vertex colors.
Let’s presume that we want to calculate the color of pixel P (or point P, however you prefer to call it). There are 2 basic methods you can use for this task: good old interpolation or a much straightforward calculation using barycentric coordinates. In this case, I will exemplify using the first method.
First of all, you need to pick a vertex (in my case, it was A). Next, you have to find the parallel line to BC which intersects your point. Knowing this line, you can then calculate its intersection with AB and AC (points M and N). Knowing these 2 points’ positions, you can determine the distance from each of them to A (MN being parallel to BC, it separates AB and AC into 2 segments whose division is proportional: BM/AM = CN/AN, but also AM/AB = AN/AC and BM/AB = CN/AC). Knowing these properties, we can easily interpolate the colors at points M and N (for example, colorM = colorA + AM/AB * (colorB – colorA) ).
Knowing the colors at M and N, we can determine the color in P. First, we calculate the distance between M and N and how P divides MN. Then, using interpolation, we calculate: colorP = colorM + MP/MN * (colorN -colorM).
Let’s touch a bit on barycentric coordinates also, which are easy to understand:
By determining the barycentric coordinate of the point in question (for example, 1/2, 1/4, 1/4), you just multiply each fraction with the color associated with each vertex and add them to determine the final color.
For example, in the previous triangle, each vertex is colored in one of the primary colors (Red, Green, Blue). The barycenter (or center of gravity – coordinates (1/3 1/3 1/3) ) will have the only grayscale color in this entire triangle: (85, 85, 85). Observe how the centers color of the edges are composed between the colors of its 2 vertices (for example, between Red and Blue, the middle color is (127, 0, 127).
Being back-end operations which you can directly influence in very small ways, there isn’t a lot to say about them, so let’s go to the next tutorial to see how to create a basic rendering game engine before we end this chapter.