PDA

View Full Version : A little confused on the purpose/intent of OpenGL VAOs/VBOs



JeffEvarts
07-16-2017, 10:11 AM
Hello everyone. I am wading into the OpenGL pool for the first time, and I think there are some "traditional" and/or "well known" things about VAOs and VBOs that aren't quite making it into the tutorials I can find.

In particular, the "bound" nature of a lot of the OpenGL stuff seems programmatically foreign to me. I suspect that it's a result of dealing with a single shared hardware resource in a multithreaded environment, but it has definitely confused me at some points.

A lot of the tutorials represent VBOs as simple arrays of floats or ints, but due to the fact that glBindBuffer takes a "purpose" flag (e.g. GL_ELEMENT_ARRAY_BUFFER) I think the VBOs also carry additional information along with them.

Likewise, VAOs are represented as 16-slot VBO holders. But again, there seems to be more to it than that, since some of the tutorials imply that shader programs are bound to a VAO.

After a while, I started to feel like a vao was "something to draw", but the VAO itself didn't seem to have a "draw" command: you needed to call the "right" function (glDrawArrays or glDrawElements) which "should" be implcit by then, given the data we've already passed down. Likewise, neither function takes a vao as an argument, which is counterintuitive. Moreso, you need to bind the element array buffer before calling glDrawElements, and (I assume) the vertex array must be bound as well at the point of drawing.

If you need to do all this vbo binding just before doing anything, what IS the point of a VAO? (Why not just do everything with momentarily bound VBOs) And, contrariwise, what's the conceptual purpose of a VBO?

Please don't shout or deride. I'm sure the answers are implied by the language in your favorite tutorial, but over the last four days I've honestly read every tutorial I can find online. Many are written by experimenters, some by professionals, and FEW by the people who actually understand the OpenGL library. Others are oversimplified, some are very abstruse, and 80% deal with versions of OpenGL that are no longer relevant. I assure you it's not as easy to acquire this knowledge as you may think.

Your help greatly appreciated

Thank you,
-Jeff Evarts, OpenGL aspirant

Dark Photon
07-16-2017, 03:20 PM
...I think there are some "traditional" and/or "well known" things about VAOs and VBOs that aren't quite making it into the tutorials I can find.

Sounds like you're doing great and not far off of understanding this.

I do understand where you're coming from. Sometimes the naming of a concept gets in the way of understanding quickly 1) what it really is and 2) why it exists.

Let's start basic. I'll simplify things just a little to avoid needless rabbit holes. If we focus just on vertex and index data need to issue draw calls...

In terms of a references diagram:


Draw call -> VAO -> Buffer Objects



Draw call = Launches a draw command using the bindings stored in the bound VAO.
VAO = Captures the vertex and index buffer object bindings and other vertex/index binding state needed to issue a draw call
Buffer objects = Holds the vertices and vertex index data used to draw your shapes.


In this case where Buffer Objects contain just vertex and/or index data for draw calls, people call these Buffer Objects "Vertex Buffer Objects", or VBOs. But this is just English naming; to GL, they're just Buffer Objects -- memory blocks of arbitrary data that you fill up yourself.

By contrast, the purpose of VAOs (Vertex Array Objects) is to function as a shorthand way for you to communicate draw command setup state to OpenGL. It's just a container (a struct) of other state. At the GL API level, you can either say: 1) "Activate VAO 5", OR 2) (equivalently) you can say "Activate the states A, B, C, D, E, F, G, H, I, J, ..." which is specified by VAO 5). #2 is just a lot more verbose and as you might expect takes more CPU time (which is the reason VAOs exist in the first place).

While you're getting started, if you want to just skip VAOs for now and come back to them later, you can. After your app starts up, just create one, bind it, forget they exist, and then go on about your program. You can come back to them later once you've got the basics down and have successfully drawn some things.


In particular, the "bound" nature of a lot of the OpenGL stuff seems programmatically foreign to me. I suspect that it's a result of dealing with a single shared hardware resource in a multithreaded environment, but it has definitely confused me at some points.

"Binding" an object just says: "OpenGL, go get this object! You're about to need it." Once the object is bound, then you issue GL calls which operate on it.

For instance, binding is like the "getObject()" lines in the code snippet below:


obj = getObject(1);
doStuff( obj );
obj = getObject(2);
doStuff( obj );
...



A lot of the tutorials represent VBOs as simple arrays of floats or ints...

Yep. Or in general, just a list of bytes. Think of it as the GPU analog to a CPU malloc()ed block of memory you can fill however you want. You can populate it with ints, floats, structures, random bytes you generate, whatever. In the case of VBOs, you fill it with the data defining vertices (and indices if you're using glDrawElements).


...but due to the fact that glBindBuffer takes a "purpose" flag (e.g. GL_ELEMENT_ARRAY_BUFFER)
Right. Think of this like OpenGL having a predefined set of N object pointers or references. For instance:


BufferObject *obj[ 10 ];

doStuff( obj[0] )
doStuff( obj[1] )


except that the elements of this array have names (e.g. GL_ELEMENT_ARRAY_BUFFER). The names are how you refer to them.

The "target" parameter (e.g. to glBindBuffer() just tells OpenGL which one of these predefined object references that you're "binding" an object to.


...I think the VBOs also carry additional information along with them.

No, nothing so strange as that. VBOs are just buffer objects, which are each just blocks of GPU memory you can fill however you want. Just think of them as malloc()ed blocks of memory on the GPU side and you're really not far off.

You can bind any buffer object to any buffer object bind target (glBindBuffer() (https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBindBuffer.xhtml)). Which one(s) you bind to when is completely determined by what you're about to direct OpenGL to do with that buffer object next.

JeffEvarts
07-16-2017, 06:48 PM
Dark Photon wrote
Sounds like you're doing great and not far off of understanding this.

I do understand where you're coming from. Sometimes the naming of a concept gets in the way of understanding quickly 1) what it really is and 2) why it exists.


Thank you SO much for "meeting me at my level" of newbieness. I know from personal experience it's hard to put yourself back in the novice mindset once the expertise has been accumulated. You are awesome.

Let me see if I have this, then:


You allocate a VBO and give it a purpose, and bind it so it's "current". This area of memory is a 'manifest, current, bound' area of memory that OpenGL (and presumably your graphics card) now knows about. Each such area is associated with a OpenGL purpose-slot-ish-thing (vertices, index offsets, textures1-4, normal vectors, whatever), and OpenGL would now know what to do with that data if you called a Draw call, since it "sees" a (bound) list of vertices and a (bound) list of index values, it "Just Knows" what to do with each one. No VAO needed! Theoretically if you called glDrawElements without first binding a vbo to GL_ELEMENT_ARRAY_BUFFER, it would barf, because OpenGL would "just know" that it didn't have all the bits it needs. This also explains why the glDrawXXX calls don't have any vao/vbo/program/texture arguments - they're all implicit from what's currently "bound".
Then, binding a VAO is merely a shorthand way of binding all the specified VBOs at once rather than looping through a list and doing it manually.
Lastly, a shader program is another OpernGL "slot" that should be bound (along with the VBOs) before you call the draw function.


If this is all true, then you, Dark Photon, are an excellent explainer.

So after all that, why ARE there different draw calls? Seems like OpenGL could figure out which one to do given the current bindings. That's just idle curiousity, btw. I think I have enough to move forward now.

Thanks.
-Jeff

Cornix
07-17-2017, 01:55 AM
OpenGL was not originally designed the way it is right now. it slowly grew over many many years. There are a lot of features which are no longer needed because we found a better way of doing things. Because of backwards compatibility however most old features are still there. In todays times you usually want to work with VBO's and VAO's and glDrawElements because they have proven to be the best way of doing things as of now. You need to keep in mind that VAO's are comparatively "young" compared to other concepts. VBO's existed before there were any VAO's.

But to reiterate what Dark Photon said:
* VBO's are raw data storage. You decide what they contain and how it is supposed to be used.
* VAO's are meta / management information for VBO's and other state. They are just a quick way to set things up. You might have 1 VAO for your static scenery, 1 VAO for your heads up display, 1 VAO for XYZ-whatever... They encapsulate a lot of state to allow you to switch states with less communication between the CPU and the GPU.
* Shaders are programs that you can write and upload to your GPU. These programs are then executed when you issue a draw call. Shaders are fed the data from your VBO's piece by piece as defined by your VAO's.

The VBO holds the data, the Shader works with the data, the VAO tells how the data is supposed to be fed into the shader (among other things).

Hope this helps.

mhagain
07-17-2017, 06:04 AM
Another important point, and one that may not be immediately obvious, is that there is not actaully a one-to-one mapping between a VBO and a draw call. It's perfectly permissable (and even quite common) for a draw call to only draw a subrange of a VBO. For example, assume that you have a VBO with 120 vertices in it. The following is perfectly legal:
glDrawArrays (GL_QUADS, 0, 4);
glDrawArrays (GL_QUADS, 12, 4);
glDrawArrays (GL_QUADS, 100, 12);One use case where this might happen is view frustum culling.

It's also possible for the same VBO to be used as a source for multiple different draws, such that it may contain both indexed and non-indexed vertices.

So hence the fact that OpenGL cannot infer your draw call type from your bound objects and state.

Dark Photon
07-17-2017, 07:19 AM
Thank you SO much for "meeting me at my level" of newbieness.

No problem!


Let me see if I have this, then
...
2. Then, binding a VAO is merely a shorthand way of binding all the specified VBOs at once rather than looping through a list and doing it manually.
3. Lastly, a shader program is another OpernGL "slot" that should be bound (along with the VBOs) before you call the draw function



Exactly. Though a VAO captures more than just the array and element buffer bindings. It also captures the info you've provided on how to parse through these buffer objects (via glVertexAttribPointer and gl{Enable,Disable}VertexAttribArray).



1. You allocate a VBO and give it a purpose, and bind it so it's "current".
Yes. You yourself give it a purpose, however as far as OpenGL calls go, you just give it "contents". There is no special "purpose" for the VBO that you provide to OpenGL. (Yes, there is a usage hint in glBufferData(), but drivers generally ignore that because so many developers set it wrong.)

Think of a malloced() block of CPU memory: it has a size (in bytes) and a handle (a pointer) you can get to it with. GL buffer objects are the same, except that the "handle" is just an integer key used by the driver to lookup the buffer object.


Each such area is associated with a OpenGL purpose-slot-ish-thing (vertices, index offsets, textures1-4, normal vectors, whatever),

Better worded: Each such area is associated with a contiguous block of bytes. There's no purpose or slot metadata inherently associated with a buffer object. It's just a dumb block of bytes!

In fact, it's most common for best performance to have a single VBO contain "all" of your vertex attributes needed in a draw call, interleaved with each other. For example, in one VBO:

* Vertex 0's position, normal, texcoord.
* Vertex 1's position, normal, texcoord.
* Vertex 2's position, normal, texcoord.
...

And as mhagain said, you can store data for multiple draw calls in one VBO. You can even stream vertex and index data into the same VBO if it's useful to you.


...and OpenGL would now know what to do with that data if you called a Draw call, since it "sees" a (bound) list of vertices and a (bound) list of index values, it "Just Knows" what to do with each one.

No. Really seriously, buffer objects are just dumb blocks of memory! You fill them with contents, and you and only you know the format of what you've put in them. You then tell OpenGL how to interpret them (i.e. where to fetch data from them and how to interpret that data).

The parameters you provide to glVertexAttribPointer (https://www.khronos.org/opengl/wiki/GLAPI/glVertexAttribPointer), gl{Enable,Disable}VertexAttribArray (https://www.khronos.org/opengl/wiki/GLAPI/glEnableVertexAttribArray), glDrawElements (https://www.khronos.org/opengl/wiki/GLAPI/glDrawElements), and glDrawArrays (https://www.khronos.org/opengl/wiki/GLAPI/glDrawArrays) tell OpenGL how to interpret the buffer object's you've bound to GL_ARRAY_BUFFER and GL_ELEMENT_ARRAY_BUFFER.

For instance, in glDrawElements (https://www.khronos.org/opengl/wiki/GLAPI/glDrawElements), the type param says what type of indices to expect, the "count" param says how many indices to expect, and the "offset" param says at what offset in the bound GL_ELEMENT_ARRAY_BUFFER OpenGL should look to find that block of "count" indices of type "type".



No VAO needed!

In the compatibility profile, no, you don't have to have a bound VAO.

In the core profile, you have to have a bound VAO. That's why I said if you just want to ignore VAOs for now, bind one on startup and then forget about them. This approach works regardless which OpenGL profile you're developing for.


Theoretically if you called glDrawElements without first binding a vbo to GL_ELEMENT_ARRAY_BUFFER, it would barf, because OpenGL would "just know" that it didn't have all the bits it needs.

In the core profile, yes.

In the compatibility profile, no.

(More detail if you care: The thing is, OpenGL didn't always have VBO support. Before that, you provided glVertexAttribPointer (https://www.khronos.org/opengl/wiki/GLAPI/glVertexAttribPointer) (and friends) and glDrawElements (https://www.khronos.org/opengl/wiki/GLAPI/glDrawElements) pointers to CPU memory buffers in your app which contained the vertex attribute and index list data, respectively, rather than binding buffer objects and providing GL offsets into those GL buffer objects. You can still do that with the compatibility profile.)


This also explains why the glDrawXXX calls don't have any vao/vbo/program/texture arguments - they're all implicit from what's currently "bound".
Right.


So after all that, why ARE there different draw calls?

Two reasons: Different use cases, and API evolution.

OpenGL has evolved over time as GPUs have gotten more and more complex and capable. Originally, draw calls were very simple. But now there's a bunch of different "options" you can specify when it's useful to you.

True, we could have a "one draw call to rule them all" with a boatload of options you have to specify. But this would make it harder for new folks to understand how the heck to use it and why it's so complex, and for devs in general to get the usage right consistently.

For now I'd just stick with glDrawArrays() and glDrawElements() until you get more familiar. Then when you start thinking: gee, I wish I could do "this" with draw calls, surf the draw calls the latest OpenGL offers and chances are you'll find one that does what you want. (For instance, search for "glDraw" here (https://www.khronos.org/registry/OpenGL-Refpages/gl4/), ignoring glDrawBuffer and glDrawBuffers as those two aren't draw calls).

JeffEvarts
07-17-2017, 12:02 PM
OpenGL was not originally designed the way it is right now. it slowly grew over many many years. There are a lot of features which are no longer needed because we found a better way of doing things. Because of backwards compatibility however most old features are still there. In todays times you usually want to work with VBO's and VAO's and glDrawElements because they have proven to be the best way of doing things as of now. You need to keep in mind that VAO's are comparatively "young" compared to other concepts. VBO's existed before there were any VAO's.

But to reiterate what Dark Photon said:
* VBO's are raw data storage. You decide what they contain and how it is supposed to be used.

OK, you and Dark Photon have been very clear on that point. I have adjusted my brain. :)


* VAO's are meta / management information for VBO's and other state. They are just a quick way to set things up. You might have 1 VAO for your static scenery, 1 VAO for your heads up display, 1 VAO for XYZ-whatever... They encapsulate a lot of state to allow you to switch states with less communication between the CPU and the GPU.

To hopefully convince you I'm actually working with you, and not merely asking you to "teach me to make swords", I'm referring to Render a triangle #2 (https://www.khronos.org/opengl/wiki/VBO_-_just_examples#Render_A_Triangle_.232) from Khronos, which affords me some relevant code.

I've just had one of those very frustrating "Aha! moments", where everything becomes totally clear, but you're fairly sure that you're still wrong. :) Familiar, anyone?

I have suddenly thought that "glBindBuffers" must actually be the (a?) purpose-assigning step that associates a particular buffer with a particular purpose. That would mean that "GL_ARRAY_BUFFER" means "data which is associated with a given vertex" and GL_ELEMENT_ARRAY_BUFFER means "indices into the vertex array". But oddly there isn't another flag for normal data, texture coordinates, etc.

And here's where it all falls down:

First, there is an absence of GL_NORMAL_BUFFER and GL_TEXTURE_COORDS_BUFFER (or, indeed, GL_TEXTURE_DATA!) and other such things that are quite obviously passed down to the drawing engine.

I also see that the very strangely named glEnableVertexAttribArray() and glVertexAttribPointer get called with some arbitrary-looking integer constants which might actually imply those sorts of things. Remembering from somewhere that VAOs have 16 slots, maybe there's a specific use for each one.

So perhaps it's a combination of glBindBuffers and glvertexAttribPointer that tells the OpenGL context how to use the various bits of the registered VBOs. If so, I probably jut need a cheat-sheet that explains the process.

This may all be terribly old hat to you folks, but unfortunately the online material is a real mishmash of eras, versions, and contradictory perspectives. I truly appreciate your collective efforts to explain.

Thank you.
-Jeff

Dark Photon
07-17-2017, 08:01 PM
..."GL_ARRAY_BUFFER" means "data which is associated with a given vertex" and
...GL_ELEMENT_ARRAY_BUFFER means "indices into the vertex array".

Close. Better worded:



Buffer objects containing vertex attribute data are bound to GL_ARRAY_BUFFER before calling glVertexAttribPointer (https://www.khronos.org/opengl/wiki/GLAPI/glVertexAttribPointer) to set up a vertex attribute array binding.
Buffer objects containing index data are bound GL_ELEMENT_ARRAY_BUFFER to set up an index array binding before calling glDrawElements (https://www.khronos.org/opengl/wiki/GLAPI/glDrawElements)


glVertexAttribPointer (https://www.khronos.org/opengl/wiki/GLAPI/glVertexAttribPointer) uses the current GL_ARRAY_BUFFER binding. That's your purpose in binding a buffer object to that buffer bind target. Similarly for glDrawElements (https://www.khronos.org/opengl/wiki/GLAPI/glDrawElements) and GL_ELEMENT_ARRAY_BUFFER.

I'd suggest you read through those man page links. Carefully note the references to GL_ARRAY_BUFFER and GL_ELEMENT_ARRAY_BUFFER in them. Hopefully a light bulb will go on for you.


But oddly there isn't another flag for normal data, texture coordinates, etc.
Right. Because:



vertex positions
vertex normals
vertex texcoords
vertex <whatevers>


are "all" vertex attributes. As such, they're each stored in their own vertex attribute array.

And GL_ARRAY_BUFFER is the buffer bind target used when setting up (binding) any vertex attribute array.

I think what would help you most right now is to trace through some simple code snippets that use glVertexAttribPointer (https://www.khronos.org/opengl/wiki/GLAPI/glVertexAttribPointer), figure them out by doing some reading, and ask questions if you still don't understand something.

For instance, take a look at these short code snippets:

* Modern OpenGL (http://github.prideout.net/modern-opengl-prezo/) (see slides 7 and 8)
* Generic_Vertex_Attribute_-_examples (https://www.khronos.org/opengl/wiki/Generic_Vertex_Attribute_-_examples)
* Modern_OpenGL_Tutorial_03#Interweaving_coordinates _and_colors (https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_03#Interweaving_coordinates _and_colors)

arekkusu
07-17-2017, 09:26 PM
I'd suggest you read through those man page links.

Or just ignore all that and read the real documentation (https://khronos.org/registry/OpenGL/specs/gl/glspec45.compatibility.pdf), from which all other man pages etc are derived. See: State Tables [Vertex Array Object State], where everything is explicitly listed (NORMAL_ARRAY_BUFFER_BINDING, etc).

JeffEvarts
07-19-2017, 11:39 AM
Or just ignore all that and read the real documentation (https://khronos.org/registry/OpenGL/specs/gl/glspec45.compatibility.pdf), from which all other man pages etc are derived. See: State Tables [Vertex Array Object State], where everything is explicitly listed (NORMAL_ARRAY_BUFFER_BINDING, etc).


OMG. This is awesome! Why isn't this THE FIRST LINK on all the "intro to OpenGL pages?!?

Thank you arekkusu!

-Jeff