OpenGL Programming/Scientific OpenGL Tutorial 05

Hidden line removal

Introduction edit

In the previous tutorial, we have drawn a three-dimensional graph using grid lines. This allowed you to look through the graph, but for more complex functions, this can be rather confusing. It would be much nicer to draw the graph as a continuous, opaque surface. In this tutorial we will see how to do that.

Front and back faces edit

Since our graph is not a close surface, but rather a sheet with some bumps in it, it is possible, depending on the orientation, to see both sides of the graph. When OpenGL draws a triangle, it automatically determines which side of the triangle is facing the camera, the front or the back face (see also the two-sided surfaces GLSL tutorial). We will change the fragment shader from the previous tutorial slightly to draw back facing triangles only half as bright as front facing triangles, making it easy to distinguish which side of the graph we are looking at. We will also introduce the uniform color, so that we can modulate the color depending on whether we are drawing the surface or the grid.

#version 120

varying vec4 graph_coord;
uniform vec4 color;

void main(void) {
  float factor;

  if(gl_FrontFacing)
          factor = 1.0;
  else
          factor = 0.5;

  gl_FragColor = (graph_coord / 2.0 + 0.5) * color * factor;
}

Drawing a surface edit

In the previous tutorial, we created a VBO which contained all the x and y coordinates of the graph. We also created an IBO that traced the horizontal and vertical grid lines. To draw a surface, we will reuse the VBO, but we will have to create a new IBO that describes how to draw the triangles that make up the surface of the graph:

GLushort indices[100 * 100 * 6];
int i = 0;

// Triangles
for(int y = 0; y < 100; y++) {
  for(int x = 0; x < 100; x++) {
    indices[i++] = y * 101 + x;
    indices[i++] = y * 101 + x + 1;
    indices[i++] = (y + 1) * 101 + x + 1;

    indices[i++] = y * 101 + x;
    indices[i++] = (y + 1) * 101 + x + 1;
    indices[i++] = (y + 1) * 101 + x;
  }
}

GLuint surface_ibo;
glGenBuffers(1, &surface_ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, surface_ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof indices, indices, GL_STATIC_DRAW);

To draw the surface with the new shader, we use the following commands:

GLfloat white[4] = {1, 1, 1, 1};
glUniform4fv(uniform_color, 1, white);

glEnableVertexAttribArray(attribute_coord2d);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glVertexAttribPointer(attribute_coord2d, 2, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, surface_ibo);
glDrawElements(GL_TRIANGLES, 100 * 100 * 6, GL_UNSIGNED_SHORT, 0);

If you do this, and rotate the graph, you will notice that depending on the orientation, the surface is not always drawn correctly. In orientations where the back most triangles are drawn first, there is no problem. However, when the front most triangles are drawn first, triangles that are more to the back can overwrite the triangles that are in the front. To prevent this from happening, we need to enable the depth buffer of course. In the main() function, use:

glutInitDisplayMode(GLUT_RGBA|GLUT_DEPTH|GLUT_DOUBLE);

And in init_resources() add:

glEnable(GL_DEPTH_TEST);

Also do not forget to clear the depth buffer before drawing a new frame:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Exercises:

  • Could we not simply reverse the order in which we draw the triangles when necessary? Perhaps with a negative stride parameter in the glVertexAttribPointer() call?
  • Try changing the order of the vertices in the triangles in various ways.
  • Try to get the same result without specifying backside triangles by using glEnable(GL_CULL_FACE) and glCullFace(GL_FRONT_AND_BACK).

Drawing the grid on top of the surface edit

Although the surface has its appeals, it is much harder to see subtle curves. It would be nice to draw the grid on top of the surface. We already know how to do that, so we just switch back to our grid IBO and draw using GL_LINES after we have drawn the surface. However, we have to give the grid lines a different color than the surface. Let's make them twice as bright as the surface:

GLfloat bright[4] = {2, 2, 2, 1};
glUniform4fv(uniform_color, 1, bright);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glDrawElements(GL_LINES, 100 * 101 * 4, GL_UNSIGNED_SHORT, 0);

When you do this, you will notice two things. The grid lines look horrible, but you can no longer see "hidden" grid lines. This is all because the depth test, by default, will ensure only fragments are drawn that are closer to the camera than any existing fragment at that position. The surface was drawn first, so no fragments will be drawn that are behind it. But if the grid lines are using the same vertices as the surface, you would expect the depth to be exactly the same. With the default depth test, you would think no grid lines should appear at all. In reality though, a line is not drawn in the same way as a triangle, and floating point rounding errors will cause some grid fragments to be slightly in front, and some slightly behind the surface.

To fix the grid lines, they should be drawn slightly closer to the camera than the surface, or the surface should be drawn slightly farther away. We could do that by applying a translation to the MVP matrix, but there is an OpenGL call that specifically addresses this problem:

glPolygonOffset(1, 1);
glEnable(GL_POLYGON_OFFSET_FILL);

This will cause triangles (but not lines or points) to be drawn with a slightly increased depth value, with the result that the grid lines now appear as they should.

Exercises:

  • Make it so you can turn the polygon offset on and off with the F4 key.
  • The default depth test function is "lesser than" (GL_LESS). Try changing it to "lesser than or equal" with glDepthFunc(GL_LEQUAL). Does it help?
  • Try different values for glPolygonOffset(), larger and/or negative.
  • Between drawing the surface and the grid, try clearing either the depth buffer or the color buffer.
  • Would it be possible to draw the grid before drawing the surface?

< OpenGL Programming

Browse & download complete code