openglrenderingstencil-buffer

opengl - how to implement "portal rendering"


I've been trying to implement something like the game Antichamber (to be more precisly this trick shown below) for the past week:

trick

Here's a video of what I'm hoping to achieve (even though it was done with the Unreal Engine 4; I'm not using that): https://www.youtube.com/watch?v=Of3JcoWrMZs

I looked up the best way to do this and I found out about the stencil buffer. Between this article and this code (the "drawPortals()" function) I found online I managed to almost implement it.

It works nicely with one portal to another room (not a crossable portal, meaning you can't walk through it and be teleported in the other room). In my example I'm drawing a portal to a simple squared room with a sphere in it; behind the portal there is another sphere, that I used to check whether the depth buffer was working correctly and drawing it behind the portal:

front

side

The problems arise when I add another portal that is near this one. In that case I manage to display the other portal correctly (the lighting is off, but the sphere on the right is of a different color to show that it's another sphere):

enter image description here

But if I turn the camera so that the first portal has to be drawn over the second one, then the depth of the first one becomes wrong, and the second portal gets drawn over the first one, like this:

enter image description here

while it should be something like this:

enter image description here

So, this is the problem. I'm probably doing something wrong with the depth buffer, but I can't find what.

My code for the rendering part is pretty much this:

glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_STENCIL_TEST);

// First portal
glPushMatrix();

// Disable writing to the color and depht buffer; disable depth testing
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);

// Make sure that the stencil always fails
glStencilFunc(GL_NEVER, 1, 0xFF);

// On fail, put 1 on the buffer
glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP);

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

// Clean the buffer
glClear(GL_STENCIL_BUFFER_BIT);

// Finally draw the portal's frame, so that it will have only 1s in the stencil buffer; the frame is basically the square you can see in the pictures
portalFrameObj1.Draw();

/* Now I compute the position of the camera so that it will be positioned at the portal's room; the computation is correct, so I'm skipping it */

// I'm going to render the portal's room from the new perspective, so I'm going to need the depth and color buffers again
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);

// Disable writing to the stencil buffer and enable drawing only where the stencil values are 1s (so only on the portal frame previously rendered)
glStencilMask(0x00);
glStencilFunc(GL_EQUAL, 1, 0xFF);

// Draw the room from this perspective
portalRoomObj1.Draw();

glPopMatrix();


// Now the second portal; the procedure is the same, so I'm skipping the comments
glPushMatrix();
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE);
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);

glStencilFunc(GL_NEVER, 1, 0xFF);
glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP);
glStencilMask(0xFF);
glClear(GL_STENCIL_BUFFER_BIT);

portalFrameObj2.Draw();

/* New camera perspective computation */

glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);

glStencilMask(0x00);
glStencilFunc(GL_EQUAL, 1, 0xFF);

portalRoomObj2.Draw();

glPopMatrix();


// Finally, I have to draw the portals' frames once again but this time on the depth buffer, so that they won't get drawn over; first off, disable the stencil buffer
glDisable(GL_STENCIL_TEST);

// Disable the color buffer
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

glClear(GL_DEPTH_BUFFER_BIT);

// Draw portals' frames
portalFrameObj1.Draw();
portalFrameObj2.Draw();

// Enable the color buffer again
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);


/* Here I draw the rest of the scene */

UPDATE

I managed to find out what the problem is, but I'm still not able to solve it. It isn't related to the depth buffer actually, but to the stencil.

Basically, the way I'm drawing the first portal is this: 1) Fill the portal frame's bits in the stencil buffer with 1s; outside the portal there are only 0s 2) Draw the portal room where the stencil has 1s (so that it gets drawn onto the frame's portal

And I repeat this for the second portal.

For the first portal, I get at step 1 something like this then (pardon the stupid drawings, I'm lazy): enter image description here

Then after step 2:
enter image description here

Then I start with the second portal:
enter image description here

But now, between step 1 and 2, I tell the stencil to draw only where the bits are 1s; since the buffer is now cleared, I lost track of the 1s of the first portal, so if part of the second portal's frame is behind the previous frame, the 1s of the second frames will still be drawn with the second portal's room, regardless of the depth buffer. Kind of like in this image: enter image description here

I don't know if I managed to explain it right...


Solution

  • I'm a few years late, but this question still shows up when searching for this problem, and I've had to fix it as well, so I figured I'll drop my solution for anyone else who comes by.

    1. Don't clear the depth buffer (except between frames)

    The main issue with your solution is that you're periodically clearing the entire depth buffer. And every time you do, you're getting rid of any information you might have to figure out which portals to draw and which are obscured. The only depth data you actually need to clobber is on the pixels where your stencils are set up; you can keep the rest.

    Just before you draw the objects "through" the portal, set up your depth buffer like so:

        // first, make sure you're writing to the depth buffer
        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
        glDepthMask(GL_TRUE);
        glStencilMask(0x00);
        // you can have the stencil enabled for this step, if you did anything fancy with it earlier (like depth testing)
        glStencilFunc(GL_EQUAL, 1, 0xFF);
        // the depth test has to be enabled to write to the depth mask. But don't worry; it'll always pass.
        glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_ALWAYS);
        // set the depth range so we only draw on the far plane, leaving a "hole" for later
        glDepthRange(1, 1);
        // now draw the portal object again
        portalFrameObj1.Draw();
        // and reset what you changed so the rest of the code works
        glDepthFunc(GL_LESS);
        glDepthRange(0, 1);
        glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE); 
    

    Now, when you draw the objects "through" the portal, they'll show up where they're needed, but the rest of the screen will still have the depth information it used to! Everybody wins!

    Of course, don't forget to overwrite the depth information with a third portalFrameObj1.Draw() like you've been doing. That'll help with the next part:

    2. Check if your portal is obscured before setting up the stencil

    At the start of the code, when you set up your stencil, you disable depth testing. You don't need to!

    glStencilOp has three arguments:

    Leave depth testing on. Set glStencilFunc(GL_ALWAYS, 0, 0xFF); rather than GL_NEVER, so the stencil test succeeds, and use dppass to set your stencil instead of sfail: glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

    Now, you could also play around with glStencilFuncSeparate to optimize things a little, but you don't need it to get portals to obscure each other.