Tips and Techniques for OpenGL Programs

  1. Developing GLSL Algorithms
    When developing GLSL code for shaders, it is sometimes possible to develop and debug the basic algorithms in regular C++ on the CPU side. Then copy and paste (with any required syntax changes) into a GLSL program. Clearly this works best for algorithms like de Casteljau curve and surface rendering where the bulk of the algorithm does not rely on special GLSL-only facilities.
  2. Debugging GLSL Algorithms Running on GPU
    A common technique when debugging GLSL code is to generate "graphical debugging output". For example, in a fragment shader, signal unusual situations or unexpected results by setting the pixel to a specific and easily recognized color. Suppose some value you are computing is supposed to be between 0 and 1. Set the pixel to green if it is less than 0; set it to red if it is greater than 1. In the resulting display, you will not only see that your error condition arose, but you will also see exactly where it occurred. That extra information is oftentimes useful when tracking down why the bug occurred.
  3. Interface Blocks
    When passing per-vertex attributes from one shader stage to the next, it is oftentimes useful to bundle them together in a struct-like fashion (to borrow a C term). This helps both in readability by making clear that a collection of attributes is logically related, and it also helps in the mechanics of variable naming, especially in situations where a shader stage must pass an incoming attribute directly out to the next stage.

    The PVAs come into the vertex shader "unbundled", but can then be copied into an interface block that will be passed on to subsequent stages. For example, consider the following global declarations in a vertex shader:

    in float granularity;
    in vec2 delta;
    in vec3 direction;
    
    out GranularityInformation
    {
        float granularity;
        vec2 delta;
        vec3 direction;
    } granOut;

    Then in, say, the main function of the vertex shader:

    granOut.granularity = granularity;
    granOut.delta = delta;
    granOut.direction = direction;

    Now suppose this information is to be used in a geometry shader and passed out of it to the next stage. We would have the following global declarations at the start of the geometry shader:

    in GranularityInformation
    {
        float granularity;
        vec2 delta;
        vec3 direction;
    } granIn[]; // the size of the array will be determined based on the layout specification for the geometry shader
    
    out GranularityInformation
    {
        float granularity;
        vec2 delta;
        vec3 direction;
    } granOut;

    Before each EmitVertex call, the geometry shader can use granIn to compute values for granOut for that vertex. Suppose a triangle is coming in, for example, and we have computed weights w0, w1, and w2 for a vertex to be emitted. We could then write:

    granOut.granularity = w0 * granIn[0].granularity + w1 * granIn[1].granularity + w2 * granIn[2].granularity;
    …
    EmitVertex();
    …

    Important Note

    The entire declaration of an interface block starting from its name ("GranularityInformation" above) to the closing brace ("}") must be exactly the same in every shader stage that uses it.

  4. Adding a per-vertex attribute
    Suppose we wish to add a per-vertex attribute (called, say, puktocity) to a ModelView subclass. For purposes of this example, we will assume "float puktocity", but the mechanism is exactly analogous for all other data types that can be used for per-vertex attributes.
    1. In the vertex shader, add the global declaration:
      in float puktocity;
      If this PVA will only be used in the vertex shader, then simply write your vertex shader code to use it as desired. Skip the rest of this step, skip step 2, and go directly to step 3.

      On the other hand, if this PVA needs to be passed to another shader stage (geometry shader, fragment shader, etc.), then it is oftentimes most convenient to use interface blocks as described above. Depending on your program, you may be able to add it to an existing one, or you may want to create a new interface block for this (and possibly other) variables.

      out ThePVABlockToBeUsed
      {
          … // other variables, if any
          float puktocity;
      } pva_out;
      Then at an appropriate place in the vertex shader code:
      pva_out.puktocity = puktocity;
    2. In a subsequent shader stage that needs to use the (potentially interpolated) value of puktocity, add the global declaration:
      in ThePVABlockToBeUsed
      {
          … // other variables, if any
          float puktocity;
      } pva_in;
      Note that pva_in will be a singleton instance as shown here in, say, the fragment shader. However, it will need to be an array (e.g., pva_in[]) in others (e.g., the geometry shader).
    3. In YourModelViewSubclass.h, you need to declare a GLuint for the VBO in the same way that your other VBO variables are declared. (Depending on how your code is set up, this may simply involve making your array of GLuints for VAOs one position longer, or you may simply add another GLuint instance variable.)
    4. In YourModelViewSubclass.c++, you need to:
      • After glGenBuffers has provided an ID for your new GLuint variable or array position in defineModel, initialize everything in the usual way by calling "the four" for this new PVA: glBindBuffer, glBufferData, glVertexAttribPointer, and glEnableVertexAttribArray.
      • Be sure to use glDeleteBuffers for this new PVA in the destructor as usual.
  5. Adding a per-primitive uniform
    Suppose we wish to add a per-primitive uniform (called, say, flag) to a ModelView subclass. For purposes of this example, we will assume "int flag", but the mechanism is exactly analogous for float, vec2, mat4, etc.
    1. In any shader that is to use this variable, add the global declaration:
      uniform int flag;
      Then simply use flag in the shader program however you wish.
    2. In YourModelViewSubclass.c++, you need to:
      • Use the appropriate glUniform* call in render before issuing any glDrawXxx calls that depend upon the uniform.
      • Programming style hint: You typically use an instance variable to hold the value you want to pass to the shaders as a uniform variable. It is good practice to have the name of your C++ instance variable be the same as that for the GLSL uniform variable. This improves code readability, and it makes it easier to spot certain types of programming errors.
  6. On Bounding Boxes
    Sometimes we wish to have ModelView instances that never contribute to the overall scene bounding box. One generic example is the following. Suppose I want to have a background that continuously changes color/shading from the top to bottom (or left to right). To do so, create a polygon of the desired size (typically larger than the rest of the scene), but implement its getMCBoundingBox method so as to return min>max for the coordinate ranges. When the Controller sees such a bounding box, it ignores the limits it returns.