CS 148 Final Project
You can see a video version of this report with a live demo here! Also, click View On GitHub above to see my source code.
In this project, I set out to recreate the graphical effect made famous by the 2007 video game Portal, by Valve Corporation. In that game, the player is able to create two portals, one blue and one orange, and these portals are "linked." So, when a player looks through the blue portal, it is as if they are looking through it and out of the orange portal, and vice versa. The screenshot below shows the effect in action, and shows one interesting aspect of the effect - portals can be seen through recursively.
This diagram shows what's actually going on here: when looking into the blue portal, the viewer's line of sight goes towards the blue portal, comes out of the orange portal and into the blue portal at a different angle, and finally exits the orange portal at a different angle.
As you might be able to imagine from the above description, this effect, like many things in graphics, would be quite easy to achieve using ray tracing - all one would have to do would be to properly generate the outgoing ray when a ray met a portal. However, since the inspiration for my project was a video game, I wanted to create this effect in real time, using rasterized graphics with OpenGL.
My basic approach to making this work was based on a comment made by one of my coworkers as I was discussing the project, and is based around the use of OpenGL's stencil buffer. The stencil buffer is similar to the depth buffer, in that the buffer does not appear on the screen, but is used to determine which future fragments will be discarded before being drawn to the screen. You can write to the stencil buffer in one draw call, and then on future draw calls it is possible to tell OpenGL to discard all fragments except those with a certain value in the stencil buffer. With this tool, I figured out a simple algorithm for rendering portals:
Draw the normal scene without portals.
Draw the portals, writing a different value to the stencil buffer for each portal - e.g. 1 for portal one, 2 for portal two.
For each portal:
a. Position the camera facing out of the other portal.
b. Render the scene, with the stencil buffer condition set to only allow fragments corresponding to the this portal.
Step 3a was by far the most difficult part of this algorithm to implement. The first problem was determining the proper angle for the view vector, as it depends both on the view vector coming into the first portal, and on the relationship between the orientations of the two portals. I did this by imagining that the second portal was placed back to back with the first, so that the view vector angle would be unmodified (as if the portals were simply a normal window). Then, I calculated what rotation would turn this imaginary second portal's orientation into the actual orientation, and applied this rotation to the incoming view vector.
The second problem was determining the proper position for the camera facing out of the other portal. Simply placing the camera at the out portal's coordinates doesn't work, since this means that objects seen through the portal stay the same size, even as the viewer moves further away from the input portal. I eventually managed to calculate the proper position by computing the vector from the viewer's position to the input portal, rotating it through the same transformation applied to the view vector, and then walking the camera back along this vector from the output portal's position. This results in a camera placed behind the output portal, standing in the same relation to it that the viewer stands to the input portal.
With the camera in the right place, all that remained was to avoid the problem of objects behind the output portal occluding the view through the portal, which I accomplished simply by setting the distance to the near plane in the projection matrix to be equal to the viewer's distance from the portal.
Rendering portals recursively becomes a bit more complicated, especially in the managing of the stencil buffer. The idea I came up with (which is actually pretty sub-optimal: see the next section) was to use the value in the stencil buffer as a bitfield, indicating exactly where in the portal hierarchy each fragment was. We saw above that three values were needed for the non-recursive case (0, 1, and 2), so I allocated two bits of the stencil buffer to each layer of portals. Thus, b00000000 represents a fragment in the "real world", b00000001 a fragment seen through portal 1, and b00000010 a fragment seen through portal 2. Then, b00000101 represents a fragment inside portal 1 inside portal 1, b00100101 a fragment inside portal 2 inside portal 1 inside portal 1, and so forth. Thus, whenever I'm rendering a view through several recursive portals, I know exactly which stencil buffer value to condition on to render only to those pixels contained within the appropriate nested set of portals. Here's a screenshot of my program rendering a "hall of mirrors" effect using recursive portals:
This approach works, and it is what is shown in my demo video and the code on GitHub. However, an obvious limitation of this approach is that it limits the depth of portal rendering - with an eight bit stencil buffer, only four levels of portals are possible. Noticing that we only need three distinct values and packing even tighter using base three gives us one more level, since 3^5 = 243 < 256, but this would be annoying to implement.
A better improvement is based on something I realized only after I implemented recursive rendering. In the absence of more than two portals, or any other reflective surfaces, it's easy to see that when you're looking through a given portal, the only portal you'll ever see inside of it is the same portal. So if you look through portal 1, your view ray comes out of portal 2, meaning that it is impossible for you to actually see portal 2. Thus, for any level after the first, only one bit is needed to represent the state of the pixel, meaning that a total of seven levels could be shown using an eight bit field.
While I did look for some prior art on rendering portals before starting my project, I intentionally didn't look at how these sources implemented portals until after I had implemented my approach, as I thought it would be interesting to compare and contrast what I came up with with what others had implemented.
My first source was a blog post by Thomas Rinsma about rendering recursive portals in OpenGL. Thomas's approach is similar to mine, but different (and superior) in an important detail: in his approach, he renders portals in a depth first manner, where the first portal is rendered and the complete view through it drawn before the second portal is rendered. This has the crucial benefit that it means he doesn't have to record different values to the stencil buffer for the different portals, since only one portal is being rendered at a time. So, the only thing he has to store in the stencil buffer is an integer representing how deep in the current portal stack that fragment is. Thus, his approach supports up to 255 levels of portals with an eight bit stencil buffer.
One very clever aspect of his approach is how he handles restoring the stencil buffer when backtracking to a higher level after rendering a (nested) portal. In my approach, I reserved a bit (or two) for each level so that this could be done with a bitmask. Thomas's approach instead increments the stencil buffer when the portal is drawn for the first time, and then when backtracking draws the same portal again, but decrementing the stencil buffer instead, thus restoring it to its previous state.
While I'm not sure exactly which approach the real game Portal used, I can make an educated guess based on the spiritual predecessor to Portal, Narbacular Drop. Narbacular Drop was a student project at Digipen with nearly identical mechanics to Portal, and the students responsible were hired by Valve to make Portal. Fortunately, Narbacular Drop's original technical design document is available online, which describes how they achieved the effect. Interestingly, they did not use the stencil buffer at all. Instead, they rendered the view from the output portal onto a texture, which was then applied to wherever the input portal was placed. This has some interesting advantages over my approach - for example, there's no need to place the portal camera behind the output portal to draw things at the proper scale, since the texture itself will automatically appear smaller when the viewer is further from the portal. It would be interesting to implement this approach and compare its performance and complexity to the stencil buffer approach.
Inspired by the game Portal (2007), by Valve Corporation.
Starter code was based off of the example code from www.learnopengl.com, by Joey de Vries.
Thomas Rinsma’s blog post on rendering recursive portals was an inspiration and point of comparison: https://th0mas.nl/2013/05/19/rendering-recursive-portals-with-opengl/
The technical design document for Narbacular Drop was used as a point of comparison: https://www.digipen.edu/fileadmin/website_data/gallery/game_websites/NarbacularDrop/
The 3D model used in the demo is “Dust Level” by eCstatic on TurboSquid, itself based on a popular Counter-Strike level.: http://www.turbosquid.com/3d-models/free-blend-model-dust-level/631230