The first is that I managed to remove the code that turned the Z-buffer on before rendering our 3D objects. As it is turned off before rendering our FPS counter that kinda broke.
The second is that inverting the modelview matrix and then transposing it does not seem to give a correct matrix usable for updating our normals. I've disabled that code for now and gone back to using the rotation part of our modelview matrix directly. As mentioned before, this will cause issues if non-uniform scaling is used but I'm not a big fan of that as it stands. I'll revisit it if I ever find a more trustworthy way of dealing with this.
The third is our specular highlight code in our fragment shader. I've added a slightly different calculation for this which basically does the same thing but I found it gave a slightly nicer highlight.
Ok, back to the topic at hand. We want to start loading more complex 3D objects from disk. This is a fairly big topic so I'll be splitting it into a few parts. This first part is mostly preparation. We need a structure to load our 3D objects into so it's time to provide this structure.
When we look at an object, say we want to render a car, it isn't a single object. The body of the car will be separate from the wheels, and the windshield, etc. Often we're talking about a collection of objects. Some 3D formats separate this out nicely, others will share vertices between objects especially when the separation is purely due to different materials being used.
In our approach each of those sub-objects will be a separate entity and in this session we'll lay the foundation for that entity. For lack of a better term I've dubbed this a mesh and we'll start work on a new library called mesh3d.h again following our single file approach.
It's object orientation, sort-of...
Before we dive into our code I want to sidestep a little to look at a general approach that I've used in a few other places as well. I'm building everything in C but there are certain concepts in object orientation that I find hard to let go. When we look at our math3d library we've used structures for our different data containers and send a pointer to the structure we're modifying as the first parameter. In fact a language such as C++ pretty much works like that behind the scenes. When you call a method on an instance you're actually calling a function where a pointer to your instance is provided as a first 'hidden parameter' also known as the this pointer. The compiler simply does a lot of additional magic to make working with your object easier. But in essence there is little difference between:
vec2* vec2Set(vec2* pSet, MATH3D_FLOAT pX, MATH3D_FLOAT pY) { pSet->x = pX; pSet->y = pY; return pSet; };and
vec2* vec2::Set(MATH3D_FLOAT pX, MATH3D_FLOAT pY) { this->x = pX; this->y = pY; return this; };But for our vector and matrix structures we're not allocation any internal information and haven't got much need for implementing constructors and destructors. As we're often setting the entire array anyway its overkill to do so.
For our mesh this does start to become important as we'll be allocating buffers. We want to make sure our variables are properly initialized so we know whether buffers have been allocated, and we want to call a "destructor" to free up any memory that has been allocated.
Now here there is a choice to make, do we want to allow for using a variable directly (stack) or do we always want to allocate the entire object (heap). C++ solves this nicely for us either by just defining a variable from our class or by using the function new to allocate. If the stack is used C++ will automatically destruct the object.
But when we look at say objective-C we can see that pointers are solely used and we actually always perform the two needed steps, first calling alloc to allocate memory, and then init, our constructor. The thing here is that we know we also need to call release once the object is not longer used to free up the memory used (not withstanding any reference counting through retain, but that is another subject).
We don't have the luxury of the compiler making the right choice so for our mesh library I've decided to go down the "always use a pointer" route but provide a single constructor call that allocates and initializes our mesh (I may change our spritesheet and tilemap libraries to follow suit). As a result you must remember to call our free function (not C's) to properly depose of the object.
Our mesh library
For this write-up I'll explain our new mesh3d.h library, as it is right now, in detailed form. We'll repeat a few things as a result of this but I think it's important to go through it. We'll then modify our current example to use the new mesh library for rendering our cube. I'm also adding a sphere because it shows the shading a little better.
We've discussed the structure of a single file implementation before but just to quickly recap, basically we are combining our header and implementation into a single file instead of two separate files as is normal. To prevent code being compiled and included multiple times we only include the implementation if MESH_IMPLEMENTATION is defined. We do this in our main.c file before including our library.
Also our mesh library uses our opengl and math3d libraries but doesn't include it, assuming it has already been included previously.
We start by defining our structures:
// structure for our vertices typedef struct vertex { vec3 V; // position of our vertice (XYZ) vec3 N; // normal of our vertice (XYZ) vec2 T; // texture coordinates (XY) } vertex; // structure for encapsulating mesh data typedef struct mesh3d { char name[50]; /* name for this mesh */ // mesh data GLuint numVertices; /* number of vertices in our object */ GLuint verticesSize; /* size of our vertices array */ vertex * verticesData; /* array with vertices, can be NULL once loaded into GPU memory */ GLuint numIndices; /* number of indices in our object */ GLuint indicesSize; /* size of our vertices array */ GLuint * indicesData; /* array with indices, can be NULL once loaded into GPU memory */ // GPU state GLuint VAO; /* our vertex array object */ GLuint VBO[2]; /* our two vertex buffer objects */ } mesh3d;Our vertex structure is the one we used before and simply combines our position, normal and texture coordinate vectors in a single entity.
Our object is defined through the mesh3d structure. This structure will grow over time but for now it contains:
- name - the name of our mesh, handy once we start having more complex 3D objects
- numVertices, verticesSize and verticesData, 3 variables that manage our vertex array while our mesh is loaded into normal memory
- numIndices, indicesSize and indicesData, 3 variables that manage our index array while our mesh is loaded into normal memory
- VAO and VBO, our two OpenGL variables for keeping track of our Vertex Array Object and two Vertex Buffer Objects which contain our mesh data once copied to our GPU
After that we start our implementation section which, as mentioned, is only included if MESH_IMPLEMENTATION is defined.
First up is our callback to error handler for logging errors:
MeshError meshErrCallback = NULL; // sets our error callback method void meshSetErrorCallback(MeshError pCallback) { meshErrCallback = pCallback; };Next we include our first 'private' function which is meshInit. meshInit will be called by our 'constructor' to initialize all our variables:
// Initialize a new mesh that either has been allocated on the heap or allocated with void meshInit(mesh3d * pMesh, GLuint pInitialVertices, GLuint pInitialIndices) { if (pMesh == NULL) { return; }; strcpy(pMesh->name, "New"); // init our vertices pMesh->numVertices = 0; pMesh->verticesData = pInitialVertices > 0 ? (vertex * ) malloc(sizeof(vertex) * pInitialVertices) : NULL; pMesh->verticesSize = pMesh->verticesData != NULL ? pInitialVertices : 0; if ((pMesh->verticesData == NULL) && (pInitialVertices!=0)) { meshErrCallback(1, "Couldn''t allocate vertex array data"); }; // init our indices pMesh->numIndices = 0; pMesh->indicesData = pInitialIndices > 0 ? (GLuint *) malloc (sizeof(GLuint) * pInitialIndices) : NULL; pMesh->indicesSize = pMesh->indicesData != NULL ? pInitialIndices : 0; if ((pMesh->indicesData == NULL) && (pInitialIndices!=0)) { meshErrCallback(2, "Couldn''t allocate index array data"); }; pMesh->VAO = GL_UNDEF_OBJ; pMesh->VBO[0] = GL_UNDEF_OBJ; pMesh->VBO[1] = GL_UNDEF_OBJ; };Our two memory arrays for vertices and indices are allocated if pInitialVertices and/or pInitialIndices are non-zero. Important here is that our numVertices/numIndices tell us how many vertices and indices we have while verticesSize/indicesSize inform us how big our memory buffer is and how many vertices and indices we can thus still store in our arrays before running out of space.
Just jumping ahead a little here, numVertices/numIndices can still be used even if our arrays have been freed up. To save on memory we allow our buffers to be freed up once we copy our mesh data to our GPU but we still need to know these values.
As 0 is a valid value for either VAO or VBO we've declared a constant to know we haven't created these object in OpenGL and initialize them as such.
Next is our 'constructor' that returns and empty mesh object:
mesh3d * newMesh(GLuint pInitialVertices, GLuint pInitialIndices) { mesh3d * mesh = (mesh3d *) malloc(sizeof(mesh3d)); if (mesh == NULL) { meshErrCallback(1, "Couldn''t allocate memory for mesh"); } else { meshInit(mesh, pInitialVertices, pInitialIndices); }; return mesh; };This allocates a memory buffer large enough for our structure and then initializes the structure by calling meshInit.
We also need a 'destructor':
// frees up data and buffers associated with this mesh void meshFree(mesh3d * pMesh) { if (pMesh == NULL) { return; }; if (pMesh->verticesData != NULL) { free(pMesh->verticesData); pMesh->numVertices = 0; pMesh->verticesSize = 0; pMesh->verticesData = NULL; }; if (pMesh->indicesData != NULL) { free(pMesh->indicesData); pMesh->numIndices = 0; pMesh->indicesSize = 0; pMesh->indicesData = NULL; }; if (pMesh->VBO[0] != GL_UNDEF_OBJ) { // these are allocated in pairs so... glDeleteBuffers(2, pMesh->VBO); pMesh->VBO[0] = GL_UNDEF_OBJ; pMesh->VBO[1] = GL_UNDEF_OBJ; }; if (pMesh->VAO != GL_UNDEF_OBJ) { glDeleteVertexArrays(1, &(pMesh->VAO)); pMesh->VAO = GL_UNDEF_OBJ; }; free(pMesh); };There is a bit more going on here as we free up any buffers we've allocated and tell OpenGL to delete our VBOs and VAO. While overkill we make sure we unset our variables as well.
Finally we free the memory related to our structure itself.
The next function adds a vertex to our vertex array:
// adds a vertex to our buffer and returns the index in our vertice buffer // return GL_UNDEF_OBJ if we couldn't allocate memory GLuint meshAddVertex(mesh3d * pMesh, const vertex * pVertex) { if (pMesh == NULL) { return GL_UNDEF_OBJ; }; if (pMesh->verticesData == NULL) { pMesh->numVertices = 0; pMesh->verticesSize = BUFFER_EXPAND; pMesh->verticesData = (vertex *) malloc(sizeof(vertex) * pMesh->verticesSize); } else if (pMesh->verticesSize <= pMesh->numVertices + 1) { pMesh->verticesSize += BUFFER_EXPAND; pMesh->verticesData = (vertex *) realloc(pMesh->verticesData, sizeof(vertex) * pMesh->verticesSize); }; if (pMesh->verticesData == NULL) { // something bad must have happened meshErrCallback(1, "Couldn''t allocate vertex array data"); pMesh->numVertices = 0; pMesh->verticesSize = 0; return GL_UNDEF_OBJ; } else { memcpy(&(pMesh->verticesData[pMesh->numVertices]), pVertex, sizeof(vertex)); return pMesh->numVertices++; /* this will return our current value of numVertices and then increase it! */ }; };It first checks if we have memory to store our vertex and allocates/expands our buffer if needed. Then it adds the vertex to our array.
We do the same for indices but add them 3 at a time (as we need 3 for every triangle):
// adds a face (3 indices into vertex array) // returns false on failure bool meshAddFace(mesh3d * pMesh, GLuint pA, GLuint pB, GLuint pC) { if (pMesh == NULL) { return false; }; if (pMesh->indicesData == NULL) { pMesh->numIndices = 0; pMesh->indicesSize = BUFFER_EXPAND; pMesh->indicesData = (GLuint *) malloc(sizeof(GLuint) * pMesh->indicesSize); } else if (pMesh->indicesSize <= pMesh->numIndices + 3) { pMesh->indicesSize += BUFFER_EXPAND; pMesh->indicesData = (GLuint *) realloc(pMesh->indicesData, sizeof(GLuint) * pMesh->indicesSize); }; if (pMesh->indicesData == NULL) { // something bad must have happened meshErrCallback(2, "Couldn''t allocate index array data"); pMesh->numIndices = 0; pMesh->indicesSize = 0; return false; } else { pMesh->indicesData[pMesh->numIndices++] = pA; pMesh->indicesData[pMesh->numIndices++] = pB; pMesh->indicesData[pMesh->numIndices++] = pC; return true; }; };
Now it's time to copy the data held within our arrays to our GPU:
// copies our vertex and index data to our GPU, creates/overwrites buffer objects as needed // if pFreeBuffers is set to true our source data is freed up // returns false on failure bool meshCopyToGL(mesh3d * pMesh, bool pFreeBuffers) { if (pMesh == NULL) { return false; }; // do we have data to load? if ((pMesh->numVertices == 0) || (pMesh->numIndices==0)) { meshErrCallback(3, "No data to copy to GL"); return false; }; // make sure we have buffers if (pMesh->VBO[0] == GL_UNDEF_OBJ) { glGenVertexArrays(1, &(pMesh->VAO)); }; if (pMesh->VBO[0] == GL_UNDEF_OBJ) { glGenBuffers(2, pMesh->VBO); }; // and load up our data // select our VAO glBindVertexArray(pMesh->VAO); // now load our vertices into our first VBO glBindBuffer(GL_ARRAY_BUFFER, pMesh->VBO[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertex) * pMesh->numVertices, pMesh->verticesData, GL_STATIC_DRAW); // now we need to configure our attributes, we use one for our position and one for our color attribute glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) 0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) sizeof(vec3)); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) sizeof(vec3) + sizeof(vec3)); // now we load our indices into our second VBO glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, pMesh->VBO[1]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * pMesh->numIndices, pMesh->indicesData, GL_STATIC_DRAW); // at this point in time our two buffers are bound to our vertex array so any time we bind our vertex array // our two buffers are bound aswell // and clear our selected vertex array object glBindVertexArray(0); if (pFreeBuffers) { free(pMesh->verticesData); // pMesh->numVertices = 0; // we do not reset this because we wish to remember how many vertices we've loaded into GPU memory pMesh->verticesSize = 0; pMesh->verticesData = NULL; free(pMesh->indicesData); // pMesh->numIndices = 0; // we do not reset this because we wish to remember how many indices we've loaded into GPU memory pMesh->indicesSize = 0; pMesh->indicesData = NULL; }; return true; };The code here is pretty much the same as it was in our previous tutorial but now copied into our library. We do reuse our VAO and VBOs if we already have them. At the end we optionally free up our arrays as we no longer need them. I've made this optional because for some effects we may wish to manipulate our mesh and copy an updated version to our GPU.
Now it's time to render our mesh:
// render our mesh bool meshRender(mesh3d * pMesh) { if (pMesh == NULL) { return false; }; if (pMesh->VAO == GL_UNDEF_OBJ) { meshErrCallback(4, "No VAO to render"); return false; } else if (pMesh->numIndices == 0) { meshErrCallback(5, "No data to render"); return false; }; glBindVertexArray(pMesh->VAO); glDrawElements(GL_TRIANGLES, pMesh->numIndices, GL_UNSIGNED_INT, 0); glBindVertexArray(0); return true; };Again this code should look familiar. We do not set up our shader nor matrices here, we assume that is handled from outside. While we'll add some material information to our mesh data later on that will be used by our shader moving this logic outside allows us to re-use our mesh for multiple purposes especially once we start looking at instancing meshes (not to be confused with instancing in OO terms).
This forms our entire mesh logic itself. For convinience I've added two support functions, one that loads our cube data into our object (meshMakeCube) and another which generates a sphere (meshMakeSphere). For these have a look at the original source code.
As time goes by I'll probably add additional primitives to our library as they can be very handy.
Putting our new library to use
Now it is time we change our example to use our new library. I've gutted all the code that generates the cube and loads it into GPU memory as we're now handling that in our our mesh3d object.
As mentioned I'm also showing a sphere for which I've add a nice little map of the earth as a texture (I'm afraid I'm not sure of the source of this image, might have come from NASA but I'm pretty sure it was made available to the public domain).
In our engine.h I've added a new enum entry in texture_types for this texture.
In our engine.c file we make a fair number of changes. First we define our global variables for our meshes:
mesh3d * cube; bool canRenderCube = true; mesh3d * sphere; bool canRenderSphere = true;Note the two canRender variables. If there is a problem loading our rendering our object it is likely it will be a problem for every frame. This will quickly clog up our logs and make it hard to find the problem. If rendering fails the first time we do not render the object again.
Next in engineSetErrorCallback we also register our callback for our mesh library.
Our load_objects function has slimmed down alot:
... cube = newMesh(24, 12); // init our cube with enough space for our buffers meshMakeCube(cube, 10.0, 10.0, 10.0); // create our cube meshCopyToGL(cube, true); // copy our cube data to the GPU sphere = newMesh(100, 100); // init our sphere with default space for our buffers meshMakeSphere(sphere, 15.0); // create our sphere meshCopyToGL(sphere, true); // copy our sphere data to the GPU ...We also load our two texture maps here.
Now it's time to render our cube which we do in engineRender as before:
// set our model matrix mat4Identity(&model); mat4Translate(&model, vec3Set(&tmpvector, -10.0, 0.0, 0.0)); // select our shader shaderSelectProgram(shaderInfo, &projection, &view, &model); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textures[TEXT_BOXTEXTURE]); glUniform1i(boxTextureId, 0); glUniform3f(lightPosId, sunvector.x, sunvector.y, sunvector.z); // now render our cube if (canRenderCube) { canRenderCube = meshRender(cube); };
We repeat the same code for our sphere but using our sphere mesh and a slightly different model matrix and voila, we have a cube and a sphere:
Download the source here
Please note that the meshAddVertex and meshAddFace functions had some dumb mistakes in reallocating memory. I've changed the code up above but at the time of writing this comment haven't updated github yet
ReplyDelete