PDA

View Full Version : Suggested VAO use for my immediate mode 2D shape library



magnanimousness
11-23-2016, 05:41 AM
Good day folks,

I'm busy learning OpenGL while trying to build my own immediate mode 2D shape drawing library. But I'm having trouble figuring out the role the VAO plays in all of this. To explain the basics of the library. The user calls DrawCircle, DrawLine, DrawRectangle etc and each of those methods in turn create a VBO and write the vertices for that shape to the VBO. Once all shapes have been "drawn", the user calls EndFrame, which renders the data to the screen. All shapes have the same basic structure: 3 float vertices which describe their position. Thats it, everything else needed to draw the shape is sent to the fragment shader in a uniform block.

Firstly, My understanding of VAOs is that they describe to the vertex shader the structure of the data sitting in the VBOs via attributes. Do I have that basic aspect correct? Secondly, If my VBOs all contain data with the same structure, I should really only need a single VAO for all shapes rendered, do I have that right?

Going on my understanding of the VAO, the workflow for the shape library works as so:

1) During instantiation of the library, I create the VAO:



glGenVertexArrays(1, &GLShapeVertexArray);
glBindVertexArray(GLShapeVertexArray);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexArrayAttrib(GLShapeVertexArray, 0);


2) whenever the user calls any of the Draw* functions I create the VBO like so:



glGenBuffers(1, &Circle.GLFillVertexBufferID);
glBindBuffer(GL_ARRAY_BUFFER, Circle.GLFillVertexBufferID);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) *(VertexCount * 3), RawVertices, GL_STATIC_DRAW);


3) When the user calls the EndFrame method, I first bind the VAO:



glBindVertexArray(GLShapeVertexArray);


4) I then iterate over the various shapes and render with:



glDrawArrays(GL_TRIANGLE_FAN, 0, Shape.VertexCount);


This method of rendering crashes quite spectacularly with an access violation trying to read address 0x0000000000 once the glDrawArrays method returns. Before I tried rendering like this, I would define a VAO per shape and bind that VAO just before drawing the shape, that worked perfectly, but I was concerned that I was doing it incorrectly.

So have I misunderstood the purpose of VAOs? If so, for this particular context, what would be a better way to handle the VAO?

Thanks,

john_connor
11-23-2016, 05:58 AM
This method of rendering crashes quite spectacularly with an access violation trying to read address 0x0000000000 once the glDrawArrays method returns.

that's because a VAO doesnt contain a single vertex, it only manages HOW data will be sent to the vertex shader
you specified a "glVertexAttribPointer(...)", but at the time there is no buffer bound at target GL_ARRAY_BUFFER, that's why your application crashes
once you issues the drawcall "glDrawArrays(...)", OpenGL tries to read data from a buffer that doesnt exist

your app should rather look like this:


GLuint vertexarray = 0;
GLuint vertexbuffer = 0;

glGenVertexArrays(1, &vertexarray);
glGenBuffers(1, &vertexbuffer);

// setup vertex array
glBindVertexArray(vertexarray);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vec3), (GLvoid*)(0));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);

// setup vertex buffer
vector<vec3> vertices = {
{ { 0, 0, 0 } },
{ { 1, 0, 0 } },
{ { 0, 1, 0 } },
};
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vec3) * vertices.size(), vertices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);


first you create the objects you need, that's 1 vertexarray (VAO) and a (vertex)buffer (VBO)
then you configure the VAO so that it reads data from the VBO
filling the VBO with data can be done later

so whats the point of using a VAO ?
you encapsulate the state on how things get sent to the shaders
imagine you have another type of shapes, 3D shapes instead of 2D
the vertex layout can differ, to draw both shape types you have to:
-- set all the attrib pointers / divisors / etc for the first state (2D rendering)
-- render all the 2D stuff
-- reset the 2D state
-- set the 3D state (again all attrib pointer etc)
-- render 3D stuff
-- reset te 3D state

to make that easier, build 2 VAOs, one for the 2D state and another for the 3D state (both could use the same VBO, but they dont have to)
-- glBindVertexArray(state2D);
-- render all the 2D stuff
-- glBindVertexArray(state3D);
-- render 3D stuff
-- glBindVertexArray(0);

magnanimousness
11-23-2016, 10:44 AM
Okay so should I call glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL); only after each shape VBO is bound? so the Draw* methods would now look like:



glGenBuffers(1, &Circle.GLFillVertexBufferID);
glBindBuffer(GL_ARRAY_BUFFER, Circle.GLFillVertexBufferID);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) *(VertexCount * 3), RawVertices, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);


For each of the shapes?

Thanks

john_connor
11-23-2016, 03:24 PM
https://www.opengl.org/wiki/Vertex_Specification_Best_Practices
https://www.opengl.org/wiki/Vertex_Rendering

no, you should only build your VAO once, and put all shapes that share a common "vertex layout" in 1 big buffer
use the code i've posted above, and put ALL your 2D shape vertices in it
keep track of the vertexoffset and vertexcount for each shape

rendering it then relatively easy:

// bind your program (shaders)
glUseProgram(program);
// bind your vertexarray
glBindVertexArray(shape2D)
// render a shape (lets say a quad)
glDrawArrays(GL_QUADS, vertexoffset, vertexcount);
--> vertexoffset means the offset in your big buffer that contains all the vertices
--> vertexcount is obviously 4

a useful new type would be:

// parameters for "glDrawArrays(...)"
struct DrawCall {
unsigned int Primitive;
unsigned int Offset;
unsigned int Count;
};

std::vector<DrawCall> mylistofshapes;

each shape can be represented as an instance of that type
another thing is:
you dont need to bind/ unbind the VAO and program for each shape, bind them once, draw ALL shapes at once, then unbind them (if necessary)

magnanimousness
11-23-2016, 09:27 PM
I see, when you say render all shapes at once, I assume you mean a single glDraw* call to render everything? If that is the case, how would I update the uniform block per shape? that uniform block contains the data for the shape background fill (amongst other things) which would be different per shape.

Also, when producing a single buffer for all shapes, I assume I write all shape data to the single VBO using glBufferSubData instead of glBufferData?

Thanks,

john_connor
11-24-2016, 04:52 AM
how about that:

// activate 2D stuff
glUseProgram(program2D);
glBindVertexArray(VAO2D);

// render all 2D shapes here
for (auto& shape : mylistofshapes)
{
// do the necessary uniform stuff here, like transformations etc

// render objects in scene
glDrawArrays(shape.Primitive, shape.VertexOffset, shape.VertexCount);
}

// do the 3D shapes
glUseProgram(program3D);
glBindVertexArray(VAO3D);

// loop over all 3D shapes including uniform stuff etc

glBindVertexArray(0);
glUseProgram(0);


glBufferData(...) allocates memory, glBufferSubData does not

the simplest way is to:

struct Vertex {
vec3 Position;
// color etc
};

std::vector<DrawCall> mylistofshapes;
std::vector<Vertex> vertices;

// triangle:
unsigned int offset = vertices.size(); // here: 0, but that works generally
vertices.push_back(Vertex{{0, 0, 0}});
vertices.push_back(Vertex{{1, 0, 0}});
vertices.push_back(Vertex{{0, 1, 0}});
unsigned int count= vertices.size() - offset ; // here: 3, but that works generally

mylistofshapes.push_back(DrawCall{GL_TRIANGLES, offset, count});

// quad
offset = vertices.size(); // here: 3, but that works generally
vertices.push_back(Vertex{{0, 0, 0}});
vertices.push_back(Vertex{{1, 0, 0}});
vertices.push_back(Vertex{{1, 1, 0}});
vertices.push_back(Vertex{{0, 1, 0}});
count= vertices.size() - offset ; // here: 4, but that works generally

mylistofshapes.push_back(DrawCall{GL_QUADS, offset, count});

// etc

finally when all shapes are in the std::vector, put them into the VBO

magnanimousness
11-24-2016, 09:30 AM
Yip, that did it! One VAO and one VBO, and it renders perfectly now. Very happy with this implementation.

Thanks a lot john_conner, really appreciate the help man!