OpenGL mirrors

A quick look on the math of arbitrary mirror planes in OpenGL.

Math

See Essential Maths for Games & Interactive Applications, pages 152-157.

At first create the mirror transformation matrix R, then calculate the translation to the center of the mirror T. The final transformation will be T*R*-T.

Implementation

In this case, each mirror has a framebuffer object which stores the mirrored content. There are two render steps: during the update phase the framebuffer gets updated. This is achieved by multiplying the all transformations with the mirror matrix R and rendering to the framebuffer. This calculation is performed only once in systems with multiple output renderings (eg. a CAVE).

During the rendering step, the mirror is rendered and a projective texturing shader is applied to the surface. Again, the reflection matrix R is applied to the calculation of the texture coordinates

Matrix Calculation

This is how the mirror class calculates the reflection matrix:
      glm::mat4 Mirror::getReflectionMatrix() const
      {
        using namespace glm;

        // reflection matrix -- see Essential Math for Games, pg 152--157
        mat3 t = mat3(1.f) - 2.f * tensorProduct(normal, normal);

        mat4 R(1.f);
        R[0] = vec4(t[0], 0.f);
        R[1] = vec4(t[1], 0.f);
        R[2] = vec4(t[2], 0.f);
        
        mat4 T(1.f);
        T[3] = vec4(center, 1.0);
       
        R[3] = vec4((mat3(1.f) - t) * center, 1.f); 
        return R;
      }
      

Mirror Shader

And here are the shaders for the projective texture lookup.
      #version 140

      varying vec4 texcoord;
      // front or backface?
      varying float face;

      uniform mat4 view;
      uniform mat4 proj;

      const mat4 scaleBias = mat4(vec4(0.5, 0.0, 0.0, 0.0), vec4(0.0, 0.5, 0.0, 0.0), vec4(0.0, 0.0, 0.5, 0.0), vec4(0.5, 0.5, 0.5, 1.0));

      void main()
      {
        gl_Position = proj * view * gl_Vertex;
            
        face = (inverse(transpose(view)) * vec4(gl_Normal, 0.0)).z;
        texcoord  = scaleBias * gl_Position; 
      }
      
      #version 120

      varying vec4 texcoord;
      varying float face;

      uniform sampler2D reflectionmap;

      void main()
      {

        vec3 color = texture2D(reflectionmap, texcoord.xy / texcoord.w).rgb;
        if (face < 0.0)
          color = vec3(0.3, 0.3, 0.3);  
        
        
        gl_FragColor = vec4(color, 1.0);
      }
      

Viewfrustum Culling

One of the standard speed improvements I implemented was view frustum culling. If the mirror is not visible, do not update it, do not render it.

There are many complex viewfrustum culling methods out there, most of them calculate the frustum in world space and while the math is not too difficult, it just fills a lot of space. More code also means a higher chance for errors, as bugs can creep in more easily.

A nice and clean version of culling is by doing it in clip space. After the vertices have been transformed by the ModelViewProjectionMatrix and before normalization, they should lie inside the clip space cube of [-w .. w] extends (or -1 .. 1 after homogenisation). A straight-forward way to implement culling is therefore:

  • Transform the vertices of the bounding box to clip space by multiplying each with the ModelViewProjectionMatrix.
  • If the vertex position (xyz) is within -w .. w it is inside the clipspace cube (and visible) otherwise not.
  • If all vertices of a bounding volume are outside, the bounding volume is invisible.
  • This works most of the time but breaks down as soon as the bounding volume takes up all the space of the screen. For example, if you go very close to such a bounding volume, it will fill the screen, but the actual vertices of the volume will be outside the frustum! In this case, this algorithm delivers a false negative result and the object disappears.

    The correct solution is only a bit more complicated in concept, but not as nice to implement. Instead of asking whether a single point is generally outside of all planes of the cube, the question gets turned around: for each plane, check whether all points are on the same side. If all points are outside of a single plane we know that they cannot be visible under any circumstances.

    This is the same idea as the 'world-space' plane approach, but we want to implement it in clipspace, to save writing. Fortunately, the excellent Lighthouse3D tutorial has a section on how to extract these planes in clip space. Essentially, we have to take single rows of the ModelViewProjectionMatrix and add the fixed third row to it. The only drawback to it is that OpenGL usually deals with column-major matrices; glm helps a lot in this case.

    
          // ClipResult is one of: INSIDE, OUTSIDE or INTERSECT
          ClipResult isVisible(const glm::mat4& mvp, const glm::vec4& min, const glm::mat4& max)
          {
            // create the eight vertices of the bounding box from min/max parameters
            vec4 vertices[] = { vec4(min.x, min.y, min.z, 1.f),
                      vec4(min.x, min.y, max.z, 1.f),
                      vec4(max.x, min.y, max.z, 1.f),
                      vec4(max.x, min.y, min.z, 1.f),
                      vec4(min.x, max.y, min.z, 1.f),
                      vec4(min.x, max.y, max.z, 1.f),
                      vec4(max.x, max.y, max.z, 1.f),
                      vec4(max.x, max.y, min.z, 1.f)};
    
    
            /* simple solution -- does not work for close objects
            for (int i = 0; i < 8; ++i)
            {
              vec4 v = mvp * vertices[i];
    
              if (v.x >= -v.w && v.x <= v.w &&
                v.y >= -v.w && v.y <= v.w && 
                v.z >= -v.w && v.z <= v.w)
                return INSIDE;
    
            }
    
            return OUTSIDE;
            */
    
            /* the six planes, based on manually extracting the mvp columns from clip space coordinates, as seen here:
              http://www.lighthouse3d.com/tutorials/view-frustum-culling/clip-space-approach-extracting-the-planes/
              Note that the article's matrix is in row-major format and therefore has to be switched. See also:
              http://www.cs.otago.ac.nz/postgrads/alexis/planeExtraction.pdf
            */
            vec4 row3 = row(mvp, 3);
            vec4 planes[] = 
            {
               row(mvp, 0) + row3,
              -row(mvp, 0) + row3,
               row(mvp, 1) + row3,
              -row(mvp, 1) + row3,
               row(mvp, 2) + row3,
              -row(mvp, 2) + row3
            };
    
    
            ClipResult result = INSIDE;
    
            // for all six planes
            for (int i = 0; i < 6; ++i)
            {
              // count how many are inside, how many are outside
              int out = 0;
              int in = 0;
    
              // for all vertices of the bounding box
              for (int j = 0; j < 8; ++j)
              {
                // test whether inside or outside
                float d = dot(planes[i], vertices[j]);
                
                if (d < 0.f)
                  ++out;
                else
                  ++in;
              }
    
              // if no point is inside the whole bounding box is invisible and we can stop here
              if (!in)
                return OUTSIDE;
              else if (out > 0)
                result = INTERSECT;
            }
    
            return result;
        }
    
         

    And that's it. It works fairly well.

    back »