(Return to list of OpenGL 3.3 tutorials)
Download (139 KB)
5413 downloads. 3 comments
Hello there! This is the 5th OpenGL 3.3 tutorial. In this one, we will try to save some memory with indexed drawing. However, this might not always be our best bet, as we will see.
In previous tutorial, we could see, that when rendering pyramid, we copied the same pieces of data over and over again. Now if we'd like to change the pyramid somehow (move one of its vertices for example), we would have to change the data in several place, or better said, in every copied instance of the vertex. And that's just not good. What we will try to do now, is to create a list of UNIQUE vertices, and then tell OpenGL indices, instead of real data, so it can dereference them to obtain real data.
For now, we leave the pyramid, and take a better object for purposes of this tutorial - a simple 4x4 heightmap. We will return to pyramid in the end, with conclusion, whether it has a meaning to do indexed drawing for it too.
Heightmap is probably the simplest way to render terrains and surfaces. As input, we have a map of heights (hence heightmap), that are distributed usually over square grid. In this tutorial, we will create a simple 4x4 heightmap manually, to see the advantage of indexed drawing (notice, that 4x4 means the size of grid, not the number of quads in heightmap - if we want to render at least one quad made from two triangles, our heightmap must be at least 2x2). These are the control points of our heightmap:
Now how to render whole heightmap: for each rendered row in heightmap, we begin with a new triangle strip. There are multiple numbers around some grid points. It just means, that they are sent more times (max twice) to the pipeline. With each new row, we must start a new triangle strip. So we are left with two options - either create a for loop, that will call rendering function (which is glDrawElements, will be explained soon), or we can somehow tell OpenGL, that after sending 8 vertices, which form a full row in heightmap, we want to RESTART our drawing. And luckily, there's a way of doing it. It's called glPrimitiveRestartIndex. This function has one parameter - index value, that doesn't address any vertex, but rather tells OpenGL, that we want to restart our drawing (like in old times calling glEnd() and then glBegin()). So when we create a list of indices in our 4x4 heightmap, numbers 0-15 will represent vertices and number 16 will represent our primitive restart index.
We want to render 3 rows with triangle strips. Each row consists of sending down eight vertices, or in this case, telling OpenGL which eight vertices to send, and after that, we want to restart primitives. This results in making list of 8x3 + 3 (for restarting) - resulting in 27 indices. A little save of space can be achieved, if we don't restart primitive after rendering last row (there's no need), saving one index, making total of 26 indices. Of course, storing indices on GPU also requires some space, but it's a LOT less than copying vertex data over and over again, and it also eases our lifes by making updating of data on-the-fly very convenient.
Now let's have a look at initializing heightmap data and indices:
glBindBuffer(GL_ARRAY_BUFFER, uiVBOHeightmapData);
glm::vec3 vHeightmapData[HM_SIZE_X*HM_SIZE_Y]; // Here the heightmap vertices will be stored temporarily
float fHeights[HM_SIZE_X*HM_SIZE_Y] =
{
4.0f, 2.0f, 3.0f, 1.0f,
3.0f, 5.0f, 8.0f, 2.0f,
7.0f, 10.0f, 12.0f, 6.0f,
4.0f, 6.0f, 8.0f, 3.0f
};
float fSizeX = 40.0f, fSizeZ = 40.0f;
FOR(i, HM_SIZE_X*HM_SIZE_Y)
{
float x = float(i%HM_SIZE_X), z = float(i/HM_SIZE_X);
vHeightmapData[i] = glm::vec3(
-fSizeX/2+fSizeX*x/float(HM_SIZE_X-1), // X Coordinate
fHeights[i], // Y Coordinate (it's height)
-fSizeZ/2+fSizeZ*z/float(HM_SIZE_Y-1) // Z Coordinate
);
}
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3)*HM_SIZE_X*HM_SIZE_Y, vHeightmapData, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
First we create VAO and VBO as always. Next, we have manually written heightmap heights. It has 4x4 = 16 values. In the following for loop, we just calculate vertices positions. We want our whole heightmap to be centered in its model coordinates, and we want its X size to be 40.0 (float fSizeX) and its Z size to be 40.0 as well, so that it will form a square (float fSizeZ). Now how to center it? Let's examine this for X axis (Z will be analogous). Calculation for X coordinate is this: Xcoord = -fSizeX/2+fSizeX*column/float(HM_SIZE_X-1). First, we move by half of total size to the left, and then we want to linearly move along the full size, depending on which column we are setting. Then we divide it by maximal possible column value, it's 4-1 = 3 (columns are numbered from 0 to 3). We have previously calculated column as i % HM_SIZE_X, for increasing values of i, it will give us numbers 0, 1, 2, 3, 0, 1, 2, 3... Hope you got it, it's good if you understand this, I know this may be trivial thing for pros, but for newcomers, the code above may seem scary, but after thinking of it for a while, the anxiety should disappear . We do the analogous thing for Z coordinates, and Y coordinates are the heights defined previously. After all's been said and done, we can upload vertex data to GPU.
Now we must deal with indices. We create a VBO for that. Notice, that we don't use GL_ARRAY_BUFFER, but GL_ELEMENT_BUFFER instead. It tells OpenGL that this buffer stores indices. Then we create (in this case manually) list of indices. Have a look at the code:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, uiVBOIndices);
int iIndices[] =
{
0, 4, 1, 5, 2, 6, 3, 7, 16, // First row, then restart
4, 8, 5, 9, 6, 10, 7, 11, 16, // Second row, then restart
8, 12, 9, 13, 10, 14, 11, 15 // Third row, no restart
};
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(iIndices), iIndices, GL_STATIC_DRAW);
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(HM_SIZE_X*HM_SIZE_Y);
As you can see, after each row there's number 16, that tells OpenGL to restart drawing. With indices ready, we upload them to GPU, and within binded VAO, these indices will be active indices. It means, that after binding this VAO and rendering using indexed drawing, these will be the indices OpenGL reads. Then we enable primitive restarting, and set restart index to 16 with glPrimitiveRestartIndex. Now we have everything set up, and we are ready to proceed with rendering.
glm::mat4 mCurrent = glm::rotate(mModelView, fRotationAngle, glm::vec3(0.0f, 1.0f, 0.0f));
glUniformMatrix4fv(iModelViewLoc, 1, GL_FALSE, glm::value_ptr(mCurrent));
glBindVertexArray(uiVAOHeightmap);
glDrawElements(GL_TRIANGLE_STRIP, HM_SIZE_X*(HM_SIZE_Y-1)*2+HM_SIZE_Y-1, GL_UNSIGNED_INT, 0);
fRotationAngle += appMain.sof(30.0f);
The main change in rendering after binding VAO is the rendering function. Now we don't call glDrawArrays, but glDrawElements instead. Now the vertices are pulled from binded index array. Number of indices (the second parameter) is number_of_rows (HM_SIZE_Y-1) times number_of_indices_per_row (HM_SIZE_X*2) + number_of_restarts (HM_SIZE_Y-1 is number of rows, and minus additional one for one saved restart at the end, resulting in HM_SIZE_Y-2). We tell OpenGL that indices are of unsigned integer type, which is partially true, because we use integers, not unsigned integers, but as long as the vertex count doesn't exceed 2^31 - 1, we don't need to worry .
There are also slight changes in shaders. We removed the input variable color, since we don't provide it, but rather we calculate the color of fragment depending on fragment's Y position. We can see that the greatest height in our heightmap is 12.0, so we just interpolate vertex position among fragments, and depending we divide it by 12.0. This gives us a slight shadow feel.
There is however one important thing we need to make clear. Vertex position is a vertex attribute. Another attributes can be color, texture coordinates and so on. But we can have only one index array for ALL vertex attributes. We cannot have separate indices for positions, colors or texture coordinates. This makes indexed rendering not appropriate for rendering some objects. In case of heightmap, it's perfect choice (or rendering closed surfaces or skin). But for objects, like our pyramid from last tutorial, it's not much of a good choice, because there are vertices with same position, but different colors. We would have to make a new vertex for each such distinct combination. And this doesn't make things easier for sure.
This is piece of code, located in win_OpenGLApp.cpp, that deals with creating window in fullscreen mode:
{
if(MessageBox(NULL, "Would you like to run in fullscreen?", "Fullscreen", MB_ICONQUESTION | MB_YESNO) == IDYES)
{
DEVMODE dmSettings = {0};
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dmSettings); // Get current display settings
hWnd = CreateWindowEx(0, sAppName.c_str(), sTitle.c_str(), WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, // This is the commonly used style for fullscreen
0, 0, dmSettings.dmPelsWidth, dmSettings.dmPelsHeight, NULL,
NULL, hInstance, NULL);
}
else hWnd = CreateWindowEx(0, sAppName.c_str(), sTitle.c_str(), WS_OVERLAPPEDWINDOW | WS_MAXIMIZE | WS_CLIPCHILDREN,
0, 0, CW_USEDEFAULT, CW_USEDEFAULT, NULL,
NULL, hInstance, NULL);
// ... function continues
}
Basically, we just ask user if he wants fullscreen mode, and if his answer is yes, then we will get current display settings, using EnumDisplaySettings function, and we will create window with size of screen resolution (dmPelsWidth and dmPelsHeight). We also change window style - we don't want any borders or title bar, we just want a plain, flat window, that will cover whole screen. And window style combination WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN is most suitable for that.
The result of all the above magic is this:
And that's all for today. Hope you learned something from this tutorial, and stay tuned for the next tutorial, in which we will cover loading textures and applying them to our objects.
Download (139 KB)
5413 downloads. 3 comments
nikola (futog@mail.ru) on 15.01.2015 04:23:20 |
glEnable(GL_PRIMITIVE_RESTART); you forgot to disable it glEnable(GL_PRIMITIVE_RESTART); glPrimitiveRestartIndex(HM_SIZE_X*HM_SIZE_Y); glDisable(GL_PRIMITIVE_RESTART); if you try to render something else without disable it... |
Sundaram on 06.04.2014 21:52:24 |
thanks for the tutorial amidst your busy schedule. I'm learning from these :) |
CarstenT on 14.12.2013 22:12:14 |
GREAT! Got hung up on the exact same setup I eventually found my error here (bytesize vs elementCount ;o/) So releaved .. |
1