Planar Reflections using the Stencil Buffer

It has been a while since my last post and to be honest, I've become a bit overwhelmed trying to juggle work and uni. This week marks the sixth week of final project, so I wanted to share my adventure implementing Anomalia's reflection mechanic. This mechanic was a brain child of mine that I'd had plans for long before final project was even in swing. The idea being that given an entry and exit point within a level that may at first appear to be inaccessible, for example up high on a platform or on the other side of a flowing stream. Certain objects such as the blue cone in the image below, would only appear in a reflection. Whether that may be an actual mirror or any reflective surface for that matter. Examples of these objects may be other platforms or stairs that lead up to the exit point, so that the player must use what they can see in a reflection to aid in completing whatever puzzle it may be.




Originally I was planing to use a Render Texture approach, rendering the scene from the viewpoint of the mirror and mapping what it sees onto the mirrored surface as a texture. Looking back now, this would have solved a host of issues that I had regarding culling. However it was a good learning experience and the solution I conjured in the end is quite flexible, given the limitations that I faced which I'll discuss shortly. Since portals had already been implemented in Anomalia using the stencil buffer, I went down this path in an attempt to reuse some of the existing code. While I was somewhat able to, it became necessary for myself and Pat (our programmer responsible for portals) to modify what was already there so that portals and reflections could coincide in the 8-bit stencil buffer.

Before discussing the implementation details with OGRE, I will explain how it works in a general sense. First the scene is rendered normally, assuming the depth and stencil buffers have been cleared and the stencil buffer is disabled. Then the stencil buffer is enabled, set to always pass and the mirror is drawn. This operation sets all of the pixels for the mirror in the stencil buffer to a different value. Next the stencil function is set to compare, the scene is flipped across the mirror plane and the depth buffer is cleared. Otherwise nothing would get drawn as the mirror would occlude everything behind it. Now we draw the flipped scene, since we set our stencil function to compare, only the pixels inside the mirror will get drawn. This is repeated for each mirror, each with a unique reference value to avoid conflicts.




It is common for the stencil increment function to be used, but in the case of this project having multiple mirrors and sharing with portals, unique values must be used. From what I have researched, rendering the mirrors first before the normal scene is also another way to approach this and seems to be a more common method. I choice the former as portals were already implemented in this fashion and to me made more sense. The stencil buffer supports the use of depth masks when reading and writing, along with reference values between 0 and 255 for an 8-bit buffer. The depth masks are how portals and reflections are able to share the limited number of bits, one bit is a flag and the remaining 7 bits allow for 0 - 127 unique portals or mirrors.

In order to flip the scene across the plane of a mirror, a number of steps must be taken. The easiest way to approach this is to first translate the mirror to the world origin. Next we must rotate the mirror, taking into account its current orientation so that it faces up, whether that may be the Y or Z axis depending on your preferred coordinate system. Now we scale the scene along that up axis by -1, this inverts or flips the scene achieving what we perceive as a reflection. Finally we rotate and translate the mirror back to its original location. Doing this will give us a matrix that we can apply to any object that we want reflected in that mirror. We also need a point on the mirror plane and its normal, used when calculating the reflection matrix and for specifying a custom clipping plane, so that objects behind the mirror in the normal scene remain obscured.




The reflection transformation in my case takes place in view space and not world space, after culling has already happened. This caused one of the main issues that took the longest to solve, since we are in view space and objects had already been culled, if an object went out of view in the normal scene it would also disappear in the reflection, note the orange sphere in the above image is missing. In theory this is a simple fix, calculate a virtual frustum from the viewpoint of the mirror and ensure that objects in this area are not culled along with the cameras primary frustum. Achieving this in OGRE however proved troublesome as I was limited to using what functions were exposed for this. In hindsight, modifying OGRE itself so that I had better access to the internals that had been abstracted away, would have been the better solution.

Given how far along in development the project was and how portals were already implemented, I decided against this. I should also note that I couldn't use the inbuilt reflection functions of OGRE as a majority of Anomalia's rendering system would have had to be rewritten. What I settled on could be considered somewhat of a hack, but an elegant one. One of the functions OGRE exposes allows a custom culling frustum to be used, independent of the viewing frustum. Unfortunately however, a typical pyramid frustum is not so ideal for volume checks as opposed to an axis aligned bounding box. Fortunately, I discovered that although the camera uses perspective projection, I was able to pass in an orthogonal culling frustum, essentially an axis aligned bound box!




Before each frame is rendered, the bounding box is calculated containing the primary camera frustum and all virtual frustums from the viewpoint of each mirror. Deriving these virtual frustums works the same as flipping objects, take the camera frustum and apply the reflection matrix to it. This bounding box is then converted to a matrix that becomes the orthogonal frustum passed in to the camera for culling. It should be obvious why I consider this a hack, albeit an efficient one. Virtual frustums are only merged into the bounding box if their respective mirror is visible in the first place. Of course there will be objects passed in that aren't actually visible, those that sit inside the boundaries but are actually between two separate frustums. I feel however that engineering a solution for this would only over complicate matters and given how fast modern GPU's are at culling off screen objects, would only slow performance down.

Comments

Popular Posts