https://learnopengl.com/Getting-started/Hello-Triangle
a vertex buffer object is our first occurrence of an opengl object as we have discussed in the opengl tutorial. just like any object in opengl this buffer has a unique ID cooresponding to that buffer, so we can generate one with a buffer ID using the glGenBuffers functions:
unsigned int VBO;
glGenBuffers(1, &VBO);
opengl has many types of buffer objects and buffer type of a vertex buffer object is GL_ARRAY_BUFFER. opengl allows us to bind to several buffers at once as long as they have a different buffer type. opengl允许我们绑定不同的buffer,只要他们的类型不同就可以。we can bind the newly created buffer to the GL_ARRAY_BUFFER target with the glBindBuffer function:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
from that point on any buffer calls we make (on the GL_ARRAY_BUFFER target) will be used to configure the currently bound buffer, which is VBO. then we can make a call to glBufferData function that copies the previously defined vertex data into the buffer’s memory:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData is a function specifically targeted to copy use-defined data into the currently bound buffer. its first argument is the type of the buffer we want to copy data into: the vertex buffer object currently bound to the GL_ARRAY_BUFFER target. the second argument specifies the size of the data (in bytes) we want to pass the buffer; a simple sizeof of the vertex data suffices. the third parameter is the actual data we want to send.
the fourth parameter specifies how we wan the graphics card to manage the given data. this can take 3 forms:
- GL_STATIC_DRAW: the data will most likely not change at all or very rarely.
- GL_DYNAMIC_DRAW: the data is likely to change a lot.
- GL_STREAM_DRAW: the data will change every time it is drawn.
the position data of the triangle does not change and stays the same for every render call so its usage type should best be GL_STATIC_DRAW. if, for instance, one would have a buffer with data that is likely to change frequently, a usage type of GL_DYNAMIC_DRAW or GL_STREAM_DRAW ensures the graphics card will place the data in memory that allows for faster writes.
as of now we stored the vertex data within memory on the graphics card as managed by a vertex buffer object named VBO. next we want to create a vertex and fragment shader that actually process this data, so let us start building those.
linking vertex attribtues
The vertex shader allows us to specify any input we want in the form of vertex attributes and while this allows for great flexibility, it does mean we have to manually specify what part of our input data goes to which vertex attribute in the vertex shader. This means we have to specify how OpenGL should interpret the vertex data before rendering.
Our vertex buffer data is formatted as follows:
The position data is stored as 32-bit (4 byte) floating point values.
Each position is composed of 3 of those values.
There is no space (or other values) between each set of 3 values. The values are tightly packed in the array.
The first value in the data is at the beginning of the buffer.
With this knowledge we can tell OpenGL how it should interpret the vertex data (per vertex attribute) using glVertexAttribPointer:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
Now that we specified how OpenGL should interpret the vertex data we should also enable the vertex attribute with glEnableVertexAttribArray giving the vertex attribute location as its argument; vertex attributes are disabled by default. From that point on we have everything set up: we initialized the vertex data in a buffer using a vertex buffer object, set up a vertex and fragment shader and told OpenGL how to link the vertex data to the vertex shader’s vertex attributes. Drawing an object in OpenGL would now look something like this:
// 0. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. use our shader program when we want to render an object
glUseProgram(shaderProgram);
// 3. now draw the object
someOpenGLFunctionThatDrawsOurTriangle();
We have to repeat this process every time we want to draw an object. It may not look like that much, but imagine if we have over 5 vertex attributes and perhaps 100s of different objects (which is not uncommon). Binding the appropriate buffer objects and configuring all vertex attributes for each of those objects quickly becomes a cumbersome process. What if there was some way we could store all these state configurations into an object and simply bind this object to restore its state?
vertex array object
a vertex array object (also known as VAO) can be bound just like a vertex buffer object and any subsequent vertex attribtue calls from that point on will be stored inside the VAO. this has the advantage that when configuring vertex attribute pointers u only have to make those calls once and whenever we want to draw the object, we can just bind the corresponding VAO. this makes switching between different vertex data and attribute configurations as easy as binding a different VAO.
A vertex array object stores the following:
1/ Calls to glEnableVertexAttribArray or glDisableVertexAttribArray.
2/ Vertex attribute configurations via glVertexAttribPointer.
3/ Vertex buffer objects associated with vertex attributes by calls to glVertexAttribPointer.
the process to generate a VAO looks similar to that of a VBO:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
To use a VAO all you have to do is bind the VAO using glBindVertexArray. From that point on we should bind/configure the corresponding VBO(s) and attribute pointer(s) and then unbind the VAO for later use. As soon as we want to draw an object, we simply bind the VAO with the prefered settings before drawing the object and that is it. In code this would look a bit like this:
// initialization code
//1. bind vertex array object
glBindVertexArray(VAO);
//2. copy our vertices array in a buffer for opengl to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//3. then set our vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// drawing code (in render loop)
//4. draw the object
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFuctionThatDrawOurTriangle();
glEnableVertexAttribArray the function glEnableVertexAttribArray enables a generic vertex arrtibute. a vertex attribute can be disabled by calling glDisableVertexAttribArray. the parameters of glEnableVertexAttribArray(GLuint index) are as follows: 1. index: specifies the index of the vertex attribute to be enabled.
and that is it! everything we did the last few million pages led up to this moment, a VAO that stores our vertex attribute configuration and which VBO to use. usually when u have multiple objects u want to draw, u first generate/configure all the VAOs (and thus the required VBO and attribute pointers) and store those for later use. the moment we want to draw one of our objects, we take the corresponding VAO, bind it, then draw the object and unbind the VAO again.
to draw over objects of choice, opengl provides us with the glDrawArrays function that draws primitives using the currently active shader, the previously defined vertex attribute configuration and with the VBO’s vertex data (indirectly bound via the VAO).
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
the glDrawArrays function takes as its first argument the opengl primitive type we would like to draw. since i said at the start we wanted to draw a triangle, and i do not like lying to u, we pass in GL_TRIANGLES. the second arguments specifies the starting index of the vertex array we would like to draw; we just leave this at 0. the last argument specifies how many vertices we want to draw, which is 3 (we only render 1 triangle from our data, which is exactly 3 vertices long).
element buffer objects
there is last thing we would like to discuss when rendering vertices and that is element buffer objects abbreviated to ebo. to explain how element buffer objects work it is best to give an example: suppose we want to draw a rectangle instead of a triangle. we can draw a rectangle using two triangles (opengl might works with triangles). this will generate the following set of vertices:
float vertices[] = {
// first triangle
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, 0.5f, 0.0f, // top left
// second triangle
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
as u can see, there is some overlap on the vertices specified. we specify bottom right and top left twice! this is an overhead of 50% since the rectangle could also be specified with only 4 vertices, instead of 6. this will only get worse as soon as we have more complex models that have over 1000s of triangles where there will be large chunks that overlap. what would be a better solution is to store only the unique vertices and then specify the order at which we want to draw these vertices in. in that case we would only have to store 4 vertices for the rectangle, and then just specify at which order we would like to draw them. would not it be great if opengl provided us with a feature like that?
thankfully, element buffer objects work exactly like that. an ebo is a buffer, just like a vertex buffer object, that stores indices that opengl uses to decide what vertcies to draw. this so called indexed drawing is exactly the solution to our problem. to get started we first have to specify the (unique) vertices and the indices to draw them as a rectanle:
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
u can see that, when using indices, we only need 4 vertices instead of 6. next we need to create the element buffer object.
unsigned int EBO;
glGenBuffers(1, &EBO);
similar to the VBO we bind the EBO and copy the indices into the buffer with glBufferData. also, just like the VBO we want to place those calls between a bind and an unbind call, although this time we specify GL_ELEMENT_ARRAY_BUFFER as the buffer type.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
Note that we’re now giving GL_ELEMENT_ARRAY_BUFFER as the buffer target. The last thing left to do is replace the glDrawArrays call with glDrawElements to indicate we want to render the triangles from an index buffer. When using glDrawElements we’re going to draw using indices provided in the element buffer object currently bound:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
the first argument specifies the mode we want to draw in, similar to glDrawArrays. the second argument is the count or number of element we would like to draw. we specified 6 indices so we want to draw 6 vertices in total. The third argument is the type of the indices which is of type GL_UNSIGNED_INT. The last argument allows us to specify an offset in the EBO (or pass in an index array, but that is when you’re not using element buffer objects), but we’re just going to leave this at 0.
The glDrawElements function takes its indices from the EBO currently bound to the GL_ELEMENT_ARRAY_BUFFER target. This means we have to bind the corresponding EBO each time we want to render an object with indices which seems again a bit cumbersome. It just so happens that a vertex array object also keeps track of element buffer object bindings. The element buffer object currently bound while a VAO is bound, is stored as the VAO’s element buffer object. Binding to a VAO thus also automatically binds its EBO.
The resulting initialization and drawing code now looks something like this:
// ..:: Initialization code :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a vertex buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. copy our index array in a element buffer for OpenGL to use
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: Drawing code (in render loop) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);