OpenGL Programming/Mini-Portal Smooth
(Work In Progress)
In the previous section, we implemented a basic working portal system.
One annoying issue is the flicker we experience when passing the portal. Let's see how to fix it.
The issue
editTo understand the flicker, we need to understand how the OpenGL camera works.
When we position the camera using its transformation matrix, we do not really position the screen itself. As we can see on the figure, the OpenGL camera is a truncated pyramid (a frustum, in maths slang). We defined it with glm::perspective
when setting the Projection matrix in the onIdle
function:
// Projection
glm::mat4 camera2screen = glm::perspective(
45.0f, // FOVy
1.0f*screen_width/screen_height, // aspect ratio
0.1f, // zNear
100.0f // zFar
);
The perspective matrix takes all objects between zNear
and zFar
, and project them on the near clipping plane. It does not display at all what is between the camera position and the near clipping plane.
The flicker happens when the camera is at a distance less than zNear
to the portal: in that case the camera position is before the portal, but the screen is behind already! The flicker is actually a quick flash of the scene behind the portal, just before we teleport the camera.
The fix
editThe intuitive idea is the teleport ahead of time: if the camera comes at a distance of zNear
close to the portal, we could teleport it straight away. This works well when facing the portal - but what about when we strafe (move sideway) through the portal? The problem will not be fixed in that case.
The real problem is mainly in the stencil buffer, given that it defines the drawn portal scene parts. The stencil buffer is built by rendering the portal onto it, and so is affected by the camera near clipping plane moving beyond the portal. If we can keep drawing on the stencil buffer when the camera's near clipping plane is behind it, we've won.
The fix we propose in this document is to make the portal volumetric, matching the volume bounded by the camera position and the near clipping plane. As the near clipping plane passes behind the portal front-face, a tailored back-face will appear and be rendered to the stencil buffer. In all directions.
Basically it's all about cheating; but we've been cheating from the start :)
Modifying zNear
editThere is still a slight issue: even if the near clipping plane is in the portal volumetric shape, there may be things inside the portal, typically the ground.
A more complete fix would be to draw the stencil buffer more precisely by computing the intersection between the portal volume and the camera near clipping rectangle.
But normally, you apply the portal on a wall, so there shouldn't be anything at all behind it.
In our demo, the ground issue can be fixed using a reasonably small value for zNear
, such as 0.01
.
/* Global */
static float zNear = 0.01;
static float fovy = 45;
With this value the ground can only be slightly noticed if the player passes the portal while looking at its feet.
It is tempting to set zNear
to a really small value, such as 0.000001
. However, if zNear is too small compared to zFar
, the depth buffer will lose its precision. You will then notice a flicker in your meshes, because triangles will alternatively be drawn one in front of the other. So don't :)
Modifying zNear
will come in handy when developping our fix, because we can set it to a large value such as 1.0
to have a clearer view of the near clipping plane postponement.
Drawing the volumetric portal
editTODO: explain maths
void create_portal(Mesh* portal, int screen_width, int screen_height, float zNear, float fovy) {
portal->vertices.clear();
portal->elements.clear();
float aspect = 1.0 * screen_width / screen_height;
float fovy_rad = fovy * M_PI / 180;
float fovx_rad = fovy_rad / aspect;
float dz = max(zNear/cos(fovx_rad), zNear/cos(fovy_rad));
float dx = tan(fovx_rad) * dz;
float dy = tan(fovy_rad) * dz;
glm::vec4 portal_vertices[] = {
glm::vec4(-1, -1, 0, 1),
glm::vec4( 1, -1, 0, 1),
glm::vec4(-1, 1, 0, 1),
glm::vec4( 1, 1, 0, 1),
glm::vec4(-(1+dx), -(1+dy), 0-dz, 1),
glm::vec4( (1+dx), -(1+dy), 0-dz, 1),
glm::vec4(-(1+dx), (1+dy), 0-dz, 1),
glm::vec4( (1+dx), (1+dy), 0-dz, 1),
};
for (unsigned int i = 0; i < sizeof(portal_vertices)/sizeof(portal_vertices[0]); i++) {
portal->vertices.push_back(portal_vertices[i]);
}
GLushort portal_elements[] = {
0,1,2, 2,1,3,
4,5,6, 6,5,7,
0,4,2, 2,4,6,
5,1,7, 7,1,3,
};
for (unsigned int i = 0; i < sizeof(portal_elements)/sizeof(portal_elements[0]); i++) {
portal->elements.push_back(portal_elements[i]);
}
}
We see that the portal depends on screen_width and screen_height (for the perspective aspect ratio). Consequently we need to rebuild the portal shape when the OpenGL window is resized - let's modify GLUT's reshape callback:
void onReshape(int width, int height) {
screen_width = width;
screen_height = height;
glViewport(0, 0, screen_width, screen_height);
create_portal(&portals[0], screen_width, screen_height, zNear, fovy);
create_portal(&portals[1], screen_width, screen_height, zNear, fovy);
}