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
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; }
#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); }
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:
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.