Seemless real time portals using stencil testing

When I first saw the portal mechanics in Valve’s Portal game, I was immediately fascinated by them. Each portal does not only show in real time what lies behind it, but also allows seemless traversal. I always wondered how exactly Valve accomplished that, until I finally tried to implement a similar behaviour myself, using my own basic game engine in C++/OpenGL.

Figure 1

The basic principle, as shown in Figure 1, is to first draw the scene normally from Cam1’s point of view. Then we transform the camera from Portal A’s coordinate system into Portal B’s coordinate system, so that it is now at the location of Cam2, looking at Portal B the same way as Cam1 is looking at Portal A, but from behind. Now we render the scene again. The problem that arises is that the second render pass simply overwrites the first one. This is where stencil testing can help out. With it we can discard all the fragments of the second render, that do not lie within Portal A’s rectangle.

glStencilMask(0xFF); // Enable writing to stencil buffer
glClear(GL_STENCIL_BUFFER_BIT);

/* Replace stencil buffer contents when depth test fails */
glStencilOp(GL_KEEP, GL_REPLACE, GL_KEEP);

glDepthFunc(GL_NEVER); // Always fail depth test

glStencilFunc(GL_ALWAYS, 1, 0xFF); // Always pass stencil test
portalA->draw(shader);

glDepthFunc(GL_LESS);

So we start out by drawing our portal first, filling the stencil buffer with 1s where it is drawn. We let it fail the depth test though, because we only need its stencil values and do not want it in the way of succeeding renders.

glStencilFunc(GL_EQUAL, 0, 0xFF); // Pass stencil test if buffer value is 0
glStencilMask(0x00); // Disable writing to stencil buffer

for (PortalSurface wall : portalSurfaces)
    wall.plane->draw(shader);

/* Always draw objects that are no walls */
glStencilFunc(GL_ALWAYS, 0, 0xFF);
box->draw(shader);
tower->draw(shader);

Next, we draw the rest of the scene. The walls of the room are drawn only where the stencil buffer is 0, effectively leaving a hole where the portal sits.

/* Increment buffer contents on depth test fail */
glStencilOp(GL_KEEP, GL_INCR, GL_KEEP);
glStencilFunc(GL_EQUAL, 1, 0xFF); // Pass stencil test if buffer value is 1
glStencilMask(0xFF); // Enable writing to stencil buffer

glDepthFunc(GL_NEVER); // Always fail depth test
portalA->draw(shader);
glDepthFunc(GL_LESS);

/* Clipping plane needs to be defined in the vertex shader */
glEnable(GL_CLIP_DISTANCE0);

glStencilMask(0x00); // Disable writing to stencil buffer

for (PortalSurface wall : portalSurfaces)
    wall.plane->draw(shader);

box->draw(shader);
tower->draw(shader);

/* Draw objects that are no walls again for buffer values of 2 */
glStencilFunc(GL_EQUAL, 2, 0xFF);
box->draw(shader);
tower->draw(shader);

glDisable(GL_CLIP_DISTANCE0);

After transforming the camera to the position behind Portal B, everything is drawn again, but only where the stencil buffer is 1. Drawing the portal this time increments the stencil buffer values to 2. We also incorporate clipping planes to exclude the wall Portal B is sitting at and everything closer to the camera. If we continue transforming the camera and repeat drawing n times, incrementing stencil buffer values, we achieve a portal depth of n:

A portal depth of 16

To be able to travel through portals seemlessly, we use an approach similar to the one we use to transform cameras for rendering portal contents. As soon as the player steps behind a portal, they are transformed into the other portal’s coordinate system, keeping their perceived position and looking direction.

The final result

Find the complete source code on Github as one of the samples of my game engine.

Latest Posts

Categories

Archives

WordPress Cookie Plugin by Real Cookie Banner