OpenGL Programming/Glescraft 5
Introduction
editIn the first tutorial, we have seen how to render a lot of cubes. In the second tutorial, we have seen how to only draw those faces of those cubes that are potentially visible. Still, when all chunks are being drawn, a lot of triangles are being processed that are either outside the screen or have their back facing the camera (and hence will be culled). If a triangle is outside the screen or is being culled, the fragment shader is never invoked, because there are no pixels to draw. However, the vertex shader still has to process all the vertices of those invisible triangles. The larger the world is, the more time is spent in the vertex shader. It is therefore better to send only those vertices to the graphics card that will actually be drawn to the screen.
Rendering only front-facing triangles
editIn almost any large 3D scene, approximately half the triangles are front-facing, the other half are back-facing. By only sending potentially front-facing triangles to the vertex shader, we effectively reduce the time spent there by half. In our voxel world, our cubes have six faces, two for each axis. Lets focus on just the x axis. If the x coordinate of the cube is less than the x coordinate of the camera then, irrespective of the direction the camera is facing, then only the cube's face pointing in the positive direction of the x axis can be visible. Similarly, if the x coordinate of the cube is greater than that of the camera, only the face pointing in the negative direction of the x axis is visible. When the x coordinate of the camera is "within" the cube, then neither face is visible.
However, it is hard to do this for each individual voxel. It is better to split the VBO of a whole chunk in two; one part for all the faces pointing in the positive x direction, the other for the faces pointing in the negative x direction. If the camera is on the positive x axis side of the chunk, we only need to render the first part of the VBO, if it is on the negative side, only the second part. When the camera's x position is "within" the chunk, then we have a problem. In that case, we can stay on the safe side by rendering the whole VBO.
We can do the same for the y and z axis.
Exercises:
- Why doesn't it matter which direction the camera is facing?
- Why is neither face visible when the camera is "within" a cube?
Rendering only chunks that are on-screen
editAlthough it is in principle possible to have a camera that has a 360 degree field of view (for example, one with a fish-eye lens), most 3D applications and games limit themselves to a camera that only has a 90 degree FOV. The main reason for this is that we are projecting a 3D world onto a flat screen that is at least some distance in front of our eyes. The wider the field of view we want to cram onto the screen, the more distortion you would introduce. The situation is different if you have a very big spherical screen, or multiple screens that go all around you, but that is outside most people's budget. If you have only one screen, a 90 degree FOV has negligible distortion. But, this means that the other 270 degrees are outside the screen. We are only wasting time by trying to render things that are outside the screen. If we would try to determine the visibility of each individual triangle, we might as well let the GPU do that for us. However, we can try to determine the visibility of whole chunks, and skip rendering those of which we are sure that no part of them is on the screen.
To check the visibility of a chunk, we look at the coordinate of its center. We can use the model-view-projection matrix to determine where on the screen the center would appear:
glm::vec3 center; // coordinates of center of chunk
glm::mat4 mvp; // model-view-projection matrix
glm::vec4 coords = mvp * glm::vec4(center, 1);
coords.x /= coords.w;
coords.y /= coords.w;
if(coords.x < -1 || coords.x > 1 || coords.y < -1 || coords.y > 1 || coords.z < 0) {
// skip this chunk
} else {
// render this chunk
}
After applying the MVP to the coordinates of the center, the result is in clip coordinates. If we then divide the x and y coordinates by the z coordinate, the x and y values are now in "normalized device coordinates", where the point (-1, -1) is the lower left corner of the window, and (1, 1) the top right corner. So if the x and y coordinates are outside that range, it is therefore outside the window. For the z coordinate, it is more useful to keep that in clip coordinates. A negative value for z means it is behind the camera, and therefore it is also not visible.
What we have neglected here is that even though the center of a chunk might be outside the visible window, some parts of the chunk can still be visible. To solve this problem, we will use a safety margin determined by the diameter of a bounding sphere that is large enough to hold a whole chunk:
float diameter = sqrtf(CX * CX + CY * CY + CZ * CZ);
if(coords.z < -diameter) {
// skip this chunk
}
diameter /= fabsf(coords.w);
if(fabsf(coords.x) > 1 + diameter || fabsf(coords.y > 1 + diameter)) {
// skip this chunk
} else {
// render this chunk
}
Note that we also need to divide the calculated diameter by the w coordinate so it is in the same coordinate space as the x and y coordinates.
Not only is this technique useful to skip drawing invisible chunks, you can also use it to only do other chunk-related work for visible chunks. When the camera suddenly turns and a whole different part of the scene is being shown, you might suddenly have to update a lot of VBOs. Because this can take a non-trivial amount of time, it makes sense to only update one or a few VBOs each frame, so the framerate will not drop. Then it also makes sense to update the VBOs of chunks that are close to the camera first. We can easily determine the distance of a chunk to the camera by computing the length of the coords vector:
float distance = glm::length(coords);
So priority should be given to chunks with the smallest distance.