«

»

Nov 09

Vertex array objects with shaders on OpenGL 2.1 & GLSL 1.2 [w/code]

rect3826Phew. Finally this is working!

I've been confined to OpenGL 2.1 and GLSL 1.2 on the Mac since the Qt OpenGL context will not pick up the core OpenGL profile (a big problem on it's own) and get an OpenGL 3.x and GLSL 1.5... So it's back to old school GL'ing, but anyway some things are working, albeit they have their quirks.
So for all of you battling the OpenGL 2.1 war, here's how I made VAOs work with a very simple shader.

The VAO vow

VAO save a ton of bit shuffling between CPU and GPU and potentially can save a lot of time if you're rendering vertex-heavy models. Plus, they are common practice in higher versions of OpenGL and non-fixed pipelines so they're simply worth the while to get into and use even in older versions.

Setting up a VAO is quite simple, although it took me a while to create a stable working version:

GLuint createVAOandVBOs(
		const unsigned int *faceArray, int numIntegersInFacesArray,
		const float *verticesArray, int numVertices,
		const float *normalsArray,
		const float *texcoordsArray
		)
{
    GLuint 	vertexLoc = 0, normalLoc = 1, texCoordLoc = 2;

    // generate Vertex Array for mesh
    GLuint VAid;
    glGenVertexArraysAPPLE(1,&VAid);
    glBindVertexArrayAPPLE(VAid);
    std::cout << "new VAO " << VAid << "\n";

    // buffer for faces
    GLuint buffer;
    glGenBuffers(1, &buffer);
    std::cout <<"new elements (faces) buffer " << buffer << " ("<<sizeof(GLuint) * numIntegersInFacesArray<<" bytes)\n";
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * numIntegersInFacesArray, faceArray, GL_STATIC_DRAW);

    // buffer for vertex positions
    if (verticesArray && numVertices > 0) {
        glGenBuffers(1, &buffer);
        std::cout <<"new position buffer " << buffer << " ("<<sizeof(GLfloat)*3*numVertices<<" bytes)\n";
        glBindBuffer(GL_ARRAY_BUFFER, buffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*3*numVertices, verticesArray, GL_STATIC_DRAW);
        glEnableVertexAttribArray(vertexLoc);
        glVertexAttribPointer(vertexLoc, 3, GL_FLOAT, GL_FALSE, 0, 0);
    }

    // buffer for vertex normals
    if (normalsArray && numVertices > 0) {
        glGenBuffers(1, &buffer);
        std::cout <<"new normal buffer " << buffer << " ("<<sizeof(GLfloat)*3*numVertices<<" bytes)\n";
        glBindBuffer(GL_ARRAY_BUFFER, buffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*3*numVertices, normalsArray, GL_STATIC_DRAW);
        glEnableVertexAttribArray(normalLoc);
        glVertexAttribPointer(normalLoc, 3, GL_FLOAT, GL_FALSE, 0, 0);
    }

    // buffer for vertex texture coordinates
    if (texcoordsArray && numVertices > 0) {
        glGenBuffers(1, &buffer);
        std::cout <<"new texcoord buffer " << buffer << " ("<<sizeof(GLfloat)*2*numVertices<<" bytes)\n";
        glBindBuffer(GL_ARRAY_BUFFER, buffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*2*numVertices, texcoordsArray, GL_STATIC_DRAW);
        glEnableVertexAttribArray(texCoordLoc);
        glVertexAttribPointer(texCoordLoc, 2, GL_FLOAT, GL_FALSE, 0, 0);
    }

    // unbind buffers
    glBindVertexArrayAPPLE(0);
    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);

    return VAid;
}

Essentially all it does is send stuff over to GPU buffers and binds everything with a single VAO.
The thing to note is the numbering of the vertex attributes, since the shader will require everything to align perfectly with attribute variables, which is a cause for pitfalls.

Shaders to go along

Now the vertex shader, pretty straightforward, just mixing in a single light source:

#version 120
attribute vec3 inPosition;   
attribute vec3 inNormal;   
attribute vec2 inTexcoord; 
 
varying vec2 texCoord; 
varying vec3 outNormal; 

varying vec3 lightDir;

void main() 
{	 
    gl_Position = gl_ModelViewProjectionMatrix * vec4(inPosition.xyz,1); 
    gl_TexCoord[0].st = inTexcoord; 
    texCoord = inTexcoord; 
    outNormal = normalize(gl_NormalMatrix * inNormal); 
    lightDir = normalize(vec3(gl_LightSource[0].position));
} 

Note the order of the attributes - it must match the order of the attributes we had when we built the VAO.
We should verify that with the OpenGL Profiler (from in the very useful XCode Graphics Tools package from Apple):
Screen Shot 2013-11-08 at 5.52.03 PM
In the "Resources" tab, selecting the GL_PROGRAM_OBJECT_ARB at the bottom we can see the attributes bound to the shader program.
I found out that if the vertex shader doesn't use the attribute in the shader code itself - it will not bind and screw up the numbering. So remember to check your shaders if things aren't working.

The fragment shader is less relevant just because it no longer cares about vertices, rather cares about pixels:

/*
adapted from http://www.lighthouse3d.com/opengl/glsl/index.php/index.php?textureComb
*/
#version 120 
 
uniform sampler2D mytex; 
varying vec2 texCoord; 
varying vec3 outNormal; 
varying vec3 lightDir;

void main() 
{ 
    vec3 cf;
    float intensity,af;
    vec4 texcol = texture2D(mytex,gl_TexCoord[0].st);  //can use our own texCoord instead...

    intensity = max(dot(lightDir,normalize(outNormal)),0.0);
	
    cf = intensity * (texcol).rgb;
    af = texcol.a;
		
    gl_FragColor = vec4(cf,af);	
}

Mostly straightforward, except some simple lighting issues.

Usage sample

Using the whole thing should be fairly simple, here using a single QUAD:

GLuint vaoid,texid,pid,mytex_location;

void init() {
    unsigned int faces[4] = {0,1,2,3};
    float vertices[12] = {0,0,0, 0,1,0, 1,1,0, 1,0,0};
    float normals[12] = {0,0,1, 0,0,1, 0,0,1, 0,0,1};
    float texcoords[8] = {0,0, 0,1, 1,1, 1,0};

    vaoid = createVAOandVBOs(faces,4,vertices,4,normals,texcoords);

   /*
   ... load a texture to texid ...
   */

   /*
   ... load the shaders to pid ...
   ... put the location of mytex sampler in mytex_location ...
   */
}

void draw() {
    glEnable(GL_TEXTURE_2D);
    glUseProgram(pid);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texid);
    glUniform1i(mytex_location , (GLint)0); // make our shader use tex unit 0

    glBindVertexArrayAPPLE(vaoid);
    glDrawElements(GL_QUADS,4,GL_UNSIGNED_INT,0);
    glBindVertexArrayAPPLE(0);

    glUseProgram(0);
    glBindTexture(GL_TEXTURE_2D,0);
}

Here showing a random wolf:
Screen Shot 2013-11-08 at 6.34.35 PM

That's about it! Enjoy
Roy.

Share
  • Mario

    Thanks for the tutorial. I spent three days pulling my hair cause my simple program didn't work, and guess what, I didn't know that the order of attributes matter XD. Sometimes small details like this can prevent a program from working. Thanks anyway for mentioning the details.

  • anand

    thanks a ton! I spent a couple of days figuring out VAO's on a MAC before realizing that I need to call glBindVertexArrayAPPLE() instead of glBindVertexArray().