I'm just taking a quick sidestep from my tutorial series to spend a few words on glActiveTexture and when to a bind texture which may have been unclear as far as this tutorial has gone.
OpenGL is a tad strange when it comes to textures. One would think that glGenTextures would return an ID that we can use whenever we identify a texture and OpenGL would sort itself out. It makes sense that we can only do operation like loading on one texture at a time and therefor need to bind it, but the rest seems a bit silly.
glGenTextures gives us IDs that allow OpenGL to identify which texture we're working with (e.g. texture objects). In theory you can have as many texture objects as you like provided you've got enough memory to load textures into.
But from the early days of OpenGL you could only use a limited number of textures at any given time simultaneously. Usually not much of a problem as most shaders only use a single texture, and more complex ones may use 3 or 4. You could have hundreds of texture objects loaded, but you'd only use a select few textures at any given time.
You could look at the set of active textures as a fixed size register and each entry in this register is identified with the GL_TEXTUREn constants (also called texture units).
So GL_TEXTURE0 is our first active texture, GL_TEXTURE1 our second, GL_TEXTURE2 our third, etc. These constants generally go up to GL_TEXTURE16 even if more are supported but you can simply do GL_TEXTURE0 + 16 to access GL_TEXTURE17.
When we want to do something with textures we therefor first need to select one of these texture units and then bind the texture object we wish to use as follows:
glActiveTexture(GL_TEXTURE0); // make our first texture unit active
glBindTexture(GL_TEXTURE_2D, TextureId); // bind the ID we got from glGenTextures
This binds our texture object id to our first texture unit. Initially as we're loading textures, we'd now configure the texture, load the texture data, etc. All these types of actions modify the texture itself, that we are using our first texture unit is simply a necessity but binding a different texture will not effect the settings and/or data we changed on our current texture.
This is why you often see in loading code that all textures are loaded while our first texture unit is active. It is often simply not even set as our first texture unit is the active one by default. In some extreme cases this can be a problem if other bits of code do not expect the texture bound to a particular unit to change. I would argue that is bad coding and you should specify the texture unit you wish to use and bind the correct texture object, not assume either is set already.
Once we're in our render loop our units become important. If we want to use 4 textures at the same time we can't simply bind one after the other. Each would overwrite the binding of the next. We need to bind our 4 textures to 4 individual units like so:
glActiveTexture(GL_TEXTURE0); // make our first texture unit active
glBindTexture(GL_TEXTURE_2D, TextureId[0]); // bind our first texture object
glActiveTexture(GL_TEXTURE1); // make our second texture unit active
glBindTexture(GL_TEXTURE_2D, TextureId[1]); // bind our second texture object
glActiveTexture(GL_TEXTURE2); // make our third texture unit active
glBindTexture(GL_TEXTURE_2D, TextureId[2]); // bind our third texture object
glActiveTexture(GL_TEXTURE3); // make our fourth texture unit active
glBindTexture(GL_TEXTURE_2D, TextureId[3]); // bind our fourth texture object
Now our four textures are all active. It will come as no surprise that it is the texture units, not the IDs generated by glGenTextures, which we subsequently use in our shaders:
glUniform1i(samplerId1, 0); // use our first texture unit for sampler 1
glUniform1i(samplerId2, 1); // use our second texture unit for sampler 2
glUniform1i(samplerId3, 2); // use our third texture unit for sampler 3
glUniform1i(samplerId4, 3); // use our fourth texture unit for sampler 4
Put the two examples together and a real life example may look something like this:
glActiveTexture(GL_TEXTURE0); // make our first texture unit active
glBindTexture(GL_TEXTURE_2D, ColorTextureID); // bind our color texture object
glUniform1i(ColorSamplerID, 0); // use our first texture unit for our color sampler
glActiveTexture(GL_TEXTURE1); // make our second texture unit active
glBindTexture(GL_TEXTURE_2D, NormalMapID); // bind our normal map texture object (bump map)
glUniform1i(NormalMapSamplerID, 1); // use our second texture unit for our normal map sampler
glActiveTexture(GL_TEXTURE2); // make our third texture unit active
glBindTexture(GL_TEXTURE_2D, EnvMapID); // bind our environment map texture object
glUniform1i(EnvMapSamplerID, 2); // use our second texture unit for our environmnet map sampler
// it is considered good sport to make our first texture unit the active one again:
glActiveTexture(GL_TEXTURE0);
It's also considered good form once you are done rendering to unbind all the textures:
glActiveTexture(GL_TEXTURE0); // make our first texture unit active
glBindTexture(GL_TEXTURE_2D, 0); // unbind
glActiveTexture(GL_TEXTURE1); // make our second texture unit active
glBindTexture(GL_TEXTURE_2D, 0); // unbind
etc...
Like I mentioned before, this may seem a little silly and bloated, why not just use our texture object IDs directly in our shaders?
There is a little more to this. There is a cost to having textures active as it enables sampling from these textures.
Having more textures active then needed may claim resources unnecessarily and slow things down especially once you hit cache limits.
At the same time, binding a new texture can result in overhead if that texture is currently not loaded into faster cache memory and needs to be brought forward.
It can be a bit of a balancing act however I'd try and abide by two rules:
- have only the textures active you need for your current shader
- minimize the number of times you need to change which textures are bound by ordering what you draw based on the shader and the texture(s) used.
This last remark is important because it feels counter productive. Especially in the past where graphics cards were much simpler or in the days that everything was done on the CPU often objects were ordered by distance to draw objects closer to the viewer first especially after Z-buffers were added into the mix. It meant the largest number of fragments could be discarded and you would save many costly calculations.
While this definately still holds true and it makes sense to draw particularly large things first (so for an FPS game, draw your ground first, your objects next and your sky last), todays graphics cards are multicore beasts making the most of parallel processing with the result that your carefully picked order of drawing objects may end up being completely distorted as polygons you've carefully put later on your list are drawn first by a core that happens to be idle at the time.
Sorting your objects to make sure you draw objects that use the same shader and same/similar textures together may result in better performance improvements then using a Z-sort.
So hope that clears up a few things that can be a little confusing.
I'll be back to my tutorials soon, I've finished about 95% of the 3D object loading code but I've yet to do a lot of cleanup of the code as it is a lot. I think I've nearly doubled the size of my example code :)
In the process I also found a few dumb mistakes, mentioned them in my previous post and a comment I added to the last part of the tutorial. I want to make sure the next version has as little bugs as possible :)
I've changed the GitHub repository for my GLFW tutorial files so that the full source is stored in the archive folder for each part. I figured it would make it easier to have a look at the source code for each part this way.
It may take a bit of time before I fix all the download links in previous parts.
Before we begin with this next session I found that there were three issues with the work so far that became apparent when I started adding a few more things.
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:
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
Next we forward declare our 'public' methods. Pretty straight forward that one.
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:
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:
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:
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:
Now that we have a structure into which we can load meshes it is time to start loading some. While we'll take it in steps next session we'll load an object from disk consisting of multiple meshes but leaving out any material information.
So we've looked at our model matrix which positions our object within our 3D space and we've looked at our projection matrix which defines our lens in a manner of speaking. The matrix we've so far left alone is our view matrix.
Our view matrix in essence defines the position and orientation of our camera in our 3D world. Well to be precise, it defines the inverse of that because we actually end up moving the entire 3D world to make our camera the center of this world.
Modifying the view matrix directly
Our first option therefor is to modify the view matrix directly. We could simply apply matrix operations onto this matrix to get the camera to move through space. We thus need to do the opposite of what we intent to do. Move the camera 10 units forward? Apply a translation to our matrix to move the world 10 units back. Rotate the camera 10 degrees to the left? We need to rotate our matrix 10 degrees to the right. Etc.
Align our view matrix with another object
Our second option is a nice one for certain types of games such as racing games. When we are sitting in the drivers seat of a car our camera is basically inside of that car. If we take the model matrix for our racing car, add a translation to move the center where the driver would be, and then inverse that matrix, we have a view matrix looking nicely out of the cars windshield. This is assuming the model of the car is properly oriented so we're not looking through the side window or the floor but that is easy to rectify.
The good old 'lookat' matrix
But the option we'll look at today is using a look-at calculation. This is a function that was available in the standard glu library and I've added to my math3d.h implementation.
The look-at calculation takes the location of your camera (or eye), the location you are looking at, and what you consider to be "up" and applies a proper view matrix.
Our changes are fairly simple. First we add our lookat and position variables to track our camera (for now globals to keep things easy):
I've also removed the code that rotates the box and instead added this code to our engineUpdate function to allow basic interaction with the camera using the WASD keys:
vec3 avector, bvector, upvector;
mat4 M;
// handle our keys....
if (engineKeyPressedCallback(GLFW_KEY_A)) {
// rotate position left
// get our (reverse) looking direction vector
vec3Copy(&avector, &camera_eye);
vec3Sub(&avector, &camera_lookat);
// rotate our looking direction vector around our up vector
mat4Identity(&M);
mat4Rotate(&M, 1.0, vec3Set(&bvector, view.m[0][1], view.m[1][1], view.m[2][1]));
// and update our eye position accordingly
mat4ApplyToVec3(&camera_eye, &avector, &M);
vec3Add(&camera_eye, &camera_lookat);
} else if (engineKeyPressedCallback(GLFW_KEY_D)) {
// rotate position right
// get our (reverse) looking direction vector
vec3Copy(&avector, &camera_eye);
vec3Sub(&avector, &camera_lookat);
// rotate our looking direction vector around our up vector
mat4Identity(&M);
mat4Rotate(&M, -1.0, vec3Set(&bvector, view.m[0][1], view.m[1][1], view.m[2][1]));
// and update our eye position accordingly
mat4ApplyToVec3(&camera_eye, &avector, &M);
vec3Add(&camera_eye, &camera_lookat);
} else if (engineKeyPressedCallback(GLFW_KEY_W)) {
// get our (reverse) looking direction vector
vec3Copy(&avector, &camera_eye);
vec3Sub(&avector, &camera_lookat);
// rotate our looking direction vector around our right vector
mat4Identity(&M);
mat4Rotate(&M, 1.0, vec3Set(&bvector, view.m[0][0], view.m[1][0], view.m[2][0]));
// and update our eye position accordingly
mat4ApplyToVec3(&camera_eye, &avector, &M);
vec3Add(&camera_eye, &camera_lookat);
} else if (engineKeyPressedCallback(GLFW_KEY_S)) {
// get our (reverse) looking direction vector
vec3Copy(&avector, &camera_eye);
vec3Sub(&avector, &camera_lookat);
// rotate our looking direction vector around our right vector
mat4Identity(&M);
mat4Rotate(&M, -1.0, vec3Set(&bvector, view.m[0][0], view.m[1][0], view.m[2][0]));
// and update our eye position accordingly
mat4ApplyToVec3(&camera_eye, &avector, &M);
vec3Add(&camera_eye, &camera_lookat);
};
// update our view matrix
mat4Identity(&view);
mat4LookAt(&view, &camera_eye, &camera_lookat, vec3Set(&upvector, 0.0, 1.0, 0.0));
Note that I am using my current view matrix to determine what my current up and right are as far as my camera is concerned and rotate around those vectors.
We're slowly getting through the boring stuff, next up is basic lighting. I'm keeping this simple with a single light source for now, we'll look at more complex lighting at some later time.
First lets go through a tiny bit of theory. Our basic lighting model will implement three types of lighting: ambient, diffuse and specular lighting.
This type of shading is known as Phong Shading
The ambient part of our lighting model is the easy part. Imagine if you will you are in a dark room and you turn on a single light that illuminate the entire room. Now hold up an object like a book and look at the side that is not facing the light. Even though no light hits the object directly from our light source you would expect it to be completely in shadow and black. Yet you'll still see its colors. This is because the light in the room bounces off all the walls and other surfaces and the object becomes lighted indirectly.
To accurately calculate this is a complex task but in computer graphics we take a rather simple shortcut by stating the object is always illuminated by a fixed amount.
The diffuse part of our lighting model requires a bit more calculation. When light hits a surface it scatters. Depending on the type of surface it can be reflected in all directions relatively evenly (and give a nice solid appearance) or be reflected in roughly a single direction (making it a mirror). More complex surfaces might even react differently for different wave lengths.
For our lighting we assume the first situation, the light gets reflected in all directions evenly. The intensity of the light being reflected is a factor of the angle at which the light hits the surface from reflecting no light if the light travels parallel to the surface to full intensity if the light hits the surface at a perfect 90 degree angle.
In order to calculate this we use the normal vector of the surface. The normal vector is a vector that is perpendicular to the surface and we simply calculate the angle between the normal vector and the vector pointing from the surface to the light source. By taking the cosine of this angle we have a great variable that is 1.0 at full illumination, 0.0 when the light travels parallel to our surface and is negative if the light lies behind the surface and we're thus in shadow.
Luckily for us calculating the cosine of a surface is incredibly simple as this happens to be the dot product of two unit vectors. So we normalize our normal vector and light direction vector and call this handy function called dot().
The specular part of our lighting is the most complex one presented here. Here we are looking at light reflecting off the surface in a specific or narrow direction. If the light reflects towards our viewpoint we see its reflection. Our normal vector again is used but now to determine the vector of the light reflected off of our surface, we have a nice function called reflect() for this.
We again calculate a cosine but now using our reflected light vector and a vector from our eye to the surface.
Because our specular calculation needs to calculating a vector from our eye/camera/viewport it makes sense to do all lighting calculations after we apply our model and view matrices but not our projection matrix. As long as we also adjust the position of our light using our view matrix we're all set.
Normal vectors
As we discussed above we need the normal vector of our surface to perform our lighting calculations. For our cube this is incredibly simple, as each face of our cube is a flat surface the normal vector applies to the entire face and can be easily calculated using the cross product of two edge vectors of our face.
But for more complex shapes this becomes a lot more complex. Also when we look at curved surfaces, even though we're rendering them with flat triangles, we can interpolate the normals between each vertex to create the illusion of a curved surface (we'll see an example of this later). For this reason OpenGL assumes we store a normal for each vertex and just like with texture mapping we have to duplicate vertexes when used for different faces with different normals.
As all this can be relatively complex it makes a lot of sense to calculate all the normals one time and store them for each vertex. To be exact, most 3D modeling software does this for us and handily store all the normals along with the model.
I've adjusted our vertex structure and vertex array to include our normals for our cube:
Note that we change our attributes so that attribute 0 remains our position, 1 becomes our normal and 2 is now our texture coordinate.
Matrices
First lets revisit our matrices for a minute. So far we've calculated our model-view-projection matrix as that is all we needed but for our light calculations we need a few more. We need our model-view matrix and we need what is called our normal matrix.
The normal matrix is a matrix that only applies our rotation and can be used to update the direction of our normal vectors.
Because we adjust everything to our view matrix we also apply our view matrix to our normal matrix. The easiest way to get your normal matrix is to take your model-view matrix and take the inner 3x3 matrix from it. This is what I normally do but it does create some issues when your model applies a non-uniform scale. Now I read somewhere that the solution is to take the inverse of your matrix and then transpose it. Since we may need the inverse of our matrix later on I've gone down this route for now but I may revert to just using the model-view matrix directly as calculating the inverse of a matrix is costly as I rarely use non-uniform scaling anyway.
Anyway, because we'll be doing these calculations a lot I've added a structure to shader.h that can store IDs for the most applicable matrices:
// typedef to obtain standard information, note that not all ids need to be present
typedef struct shaderStdInfo {
GLuint program;
GLint projectionMatrixId; // our projection matrix
GLint viewMatrixId; // our view matrix
GLint modelMatrixId; // our model matrix
GLint modelViewMatrixId; // our model view matrix
GLint modelViewInverseId; // inverse of our model view matrix
GLint normalMatrixId; // matrix to apply to our normals
GLint mvpId; // our model view projection matrix
} shaderStdInfo;
There is also a function called shaderGetStdInfo() that populates this structure.
Finally I've added a function called shaderSelectProgram() that binds the shader program referenced by our structure and then calculates and applies all our matrices from a model, view and projection matrix that is passed to it.
What is very important to know is that GLSL will remove any uniform that isn't actually used in the source code so there is no use defining say modelViewInverse if you're not using it.
While the code logs that it doesn't exist shaderSelectProgram() simply skips those.
There is a way to use a VBO to load all matrices in one go which I may look into at a later date.
Our load_shaders function in engine.c now calls our shaderGetStdInfo function. We still have two uniforms that fall outside of our structure: our light position and our texture sampler (and there are several other uniforms in the shader.
Our engineRender function similarly now calls shaderSelectProgram.
Our new shaders
The real magic however happens inside of our shaders. The changes to our vertex shader are very straight forward. First we now have an attribute for our normals.
We also have 2 new outputs, one for our position (V) and one for our normal (N). For both the correct matrix is applied.
Our fragment shader has grown substantially, we'll look at each part individually:
#version 330
// info about our light
uniform vec3 lightPos; // position of our light after view matrix was applied
uniform float ambient = 0.3; // ambient factor
uniform vec3 lightcol = vec3(1.0, 1.0, 1.0); // color of the light of our sun
// info about our material
uniform sampler2D boxtexture; // our texture map
uniform float shininess = 50.0; // shininess
in vec4 V; // position of fragment after modelView matrix was applied
in vec3 N; // normal vector for our fragment
in vec2 T; // coordinates for this fragment within our texture map
out vec4 fragcolor; // our output color
We have a couple of new uniforms. Note that we've added default values for a couple of them so you can set them from code but don't have to:
lightPos - the position of our lightsource with view matrix applied to it
ambient - our ambient factor, our default is 30%
lightcol - the color of our light source, white for now
boxtexture - we had this one already, our texture sampler
shininess - the shininess for our specular lighting
Our input variables also match the output variables of our vertex shader.
void main() {
// start by getting our color from our texture
fragcolor = texture(boxtexture, T);
if (fragcolor.a < 0.5) {
discard;
};
// Get the normalized directional vector between our surface position and our light position
vec3 L = normalize(lightPos - V.xyz);
We start by getting our color from our texture map as before. Then we calculate vector L as a normalized directional vector that points from our surface to our lightsource. This vector we'll need in both our diffuse and specular calculations.
// We calculate our ambient color
vec3 ambientColor = fragcolor.rgb * lightcol * ambient;
This is our ambient color calculation, we simply multiply our surface color with our light color and our ambient factor.
// We calculate our diffuse color, we calculate our dot product between our normal and light
// direction, note that both were adjusted by our view matrix so they should nicely line up
float NdotL = max(0.0, dot(N, L));
// and calculate our color after lighting is applied
vec3 diffuseColor = fragcolor.rgb * lightcol * (1.0 - ambient) * NdotL;
For our diffuse color we first calculate our dot product for our normal and light vector.
We then multiply our surface color with our light color and our dot product taking out the ambient factor we've already used.
For our specular highlight we first calculate our reflection vector then calculate our dot product using our position.
Finally we apply our shininess using the power function and multiply the outcome with our light color. The higher the shininess value the smaller our reflection.
We only do this if we have a shininess value and if we're not in shadow.
Note that we do not apply our texture color here because we are reflecting our light. An additional specular color related to our surface material may be applied here or even a separate specular texture map but I've left that out in our example.
// and add them all together
fragcolor = vec4(clamp(ambientColor+diffuseColor+specColor, 0.0, 1.0), 1.0);
The last step is to add all these colors together. The clamp function we call here makes sure our color does not overflow.
And the end result:
A box really is a terrible shape to show off the lighting, as the normals for each face are all parallel we're basically 'flat shading' this cube. Once we start loading more complex shapes it should look a lot nicer. Also with the specular highlighting implemented the way it is the box has a mirror finish, not really something suitable for cardboard.
You could easily extent the shader to allow for more then one lightsource by simply repeating the ambient, diffuse and specular lighting calculations for those other light sources and just adding the results together. There are a number of additional parameters that you could add to improve on this One I already mentioned is the surface materials specular color. Another that is relatively simple to add are restraints on the angle to the lightsource and a limit to the distance to the lightsource to create a spotlight type light source.
There are many other shader techniques that are worth learning:
Bump mapping or normal mapping which is a technique to simulate groves and imperfections on the surface by adjusting the surface normal. This can be achieved by using a texture encoding normals that you look up instead of using our vertex normals.
Environment mapping is a cool way to do reflections or to solve our ambient issue discussed above. Ever looked at a CGI movies documentary and wondered why they hold up a mirror ball and take a picture? Use that picture as a texture and use your normal vectors X and Y (ignoring it's Z) as a lookup (adjusted so 0.0 is at the center of that image) and voila.
Shadow maps, so objects cast shadows onto other object, but that's a topic for later.
What's next
Now that we've got our basic rendering of 3D objects all sorted the next quick detour will be looking at moving the camera around.
We also need to write something that will let us load 3D objects from disk instead of hardcoding them in source code.
So that will be our next two subjects, hopefully after that we'll jump back into our platform game.
Now that we have the basic logic for displaying a 3D object it is time to slowly make it look nicer.
We'll start by adding a texture map to our object. We'll be replacing the funky color logic here. It would be easy to combine the two but I see little point in doing so. The color of an object is usually uniform (lighting not withstanding) and the texture map replaces that function. But there are nice things you can do with mixing color in especially to create certain lighting effects. Such colors more often then not aren't set per vertex but instead set as uniforms.
That reminds me about a quick sidestep I'm not sure I made clear enough in previous examples. Our shaders have 4 prefixes for "global" (global to the shader) variables:
uniform, this means the value of the variable does not change and is generally set from outside of the shader, i.e. we set in in code
layout, which points to one of our attributes in our vertex buffer object(s)
out, which is an output variable which allows us to set variables and give them to the next shader in the pipeline. As mentioned before, these variables are often interpolated
in, which is an input variable which should match the output of the previous shader in our pipeline. So the output of our vertex shader becomes the input of our fragment shader.
There aren't many things we need to change from our previous example.
Our texture map
First we need to load our texture map, I've added a box texture I grabbed somewhere off of the internet where we have different textures for each side of our box and placed this into our resources folder and then re-introduced a few things from our earlier tutorials.
First in our engine.h I've defined an enumeration that I use in conjunction with an array of textures. This is just convenience. Seeing we only have one texture it is also overkill but I like to be prepared:)
Then in engine.c I've defined the texture array into which we'll load our texture ids:
GLuint textures[TEXT_COUNT] = { 0 };
Now we need to load our texture, I've reintroduced my setTexture helper function and added the required logic at the end of our load_objects function:
void setTexture(GLuint pTexture, GLint pFilter, GLint pWrap) {
glBindTexture(GL_TEXTURE_2D, pTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, pFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, pWrap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, pWrap);
};
void load_objects() {
int x, y, comp;
unsigned char * data;
...
// Now lets load our textures, note that this does not relate to our VAO state
glGenTextures(TEXT_COUNT, textures);
// and we load our box texture into textures[TEXT_BOXTEXTURE]
data = stbi_load("boxtexture.jpg", &x, &y, &comp, 4);
if (data == 0) {
engineErrCallback(-1, "Couldn't load boxtexture.jpg");
} else {
setTexture(textures[TEXT_BOXTEXTURE], GL_LINEAR, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
};
};
void unload_objects() {
glDeleteTextures(TEXT_COUNT, textures);
glDeleteBuffers(2, VBOs);
glDeleteVertexArrays(1, &VAO);
};
Note also that we delete our textures on our unload.
Changes to our model
Next we need to change our model data to contain coordinates within our texture for each vertex. Note that our coordinates range from (0.0, 0.0) - (1.0, 1.0) and are scaled up to the resolution of the texture image. (0.0, 0.0) is the top left of the image and (1.0, 1.0) is the bottom right. When loading the texture we told it to clamp to the edge but you can also set it to wrap the texture so it can be used as a pattern.
Now we do have a problem in the way we apply our texture because we only have 8 vertexs, we'll come back to that later. We'll change our vertex structure to hold a texture coordinate T instead of our color C and then adjust our vertex array:
Our index array stays the way it is for now but we do need to change our vertex attribute pointers in our load_objects function:
// now we need to configure our attributes, we use one for our position and one for our color attribute
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) 0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) sizeof(vec3));
Note, the only change here is that our 2nd attribute is now 2 floats instead of 3 (the 2nd parameter in the last call).
Changes to our shaders
Now we need to update our shader code, again only small changes are needed. Our vertex shader now looks like this:
#version 330
layout (location=0) in vec3 positions;
layout (location=1) in vec2 texcoords;
uniform mat4 mvp; // our model-view-projection matrix
out vec2 coords;
void main(void) {
// load up our values
vec4 V = vec4(positions, 1.0);
coords = texcoords;
// projection of V
gl_Position = mvp * V;
}
The only change here is that our 2nd attribute has been renamed and is now a vec2 and our output variable is changed in unison to output a texture coordinate instead of a color. Just like with our color variable the values will be interpolated between the vertices before being sent to our fragment shader.
Our fragment shader has a few more changes:
#version 330
uniform sampler2D boxtexture;
in vec2 coords;
out vec4 fragcolor;
void main() {
fragcolor = texture(boxtexture, coords);
if (fragcolor.a < 0.5) {
discard;
}
}
Here we see the definition of a sampler2D uniform which is what we'll bind our texture to.
It is then used in our shader function together with our coordinate input variable "coords" to lookup our color.
The alpha check isn't needed for what we're doing now but a nice one to have, this allows us to do render shapes like leafs on a tree without having an elaborate model (see my tree generation blog post from a few months back).
Finally when loading our shader we need to grab the ID of our boxtexture sampler uniform in our load_shaders function:
Finally to render our cube with a texture we need to bind our texture and inform our shader to use it. As I mentioned before, the texture is not part of our VAO state and thus not loaded automatically when we bind our VAO. This is actually a handy thing because we could reuse our cube model to render multiple cubes with different textures.
All we need to add is a little bit of code to our engine_render function when we select our shader:
However we're not there yet, only the front and back of the box is properly textured. The sides, top and bottom are all wrong. Here we have a problem that we often run into because we need different texture coordinates for the same position when rendering the other sides of the cube.
What we see in many 3D formats is that our index array actually contains multiple indexes for each vertex of the model pointing to separate position and texture coordinate arrays. This is not something OpenGL is designed to do.
Our only option is to duplicate vertices. This may result in some more data but its a small price to pay. I'm creating duplicates each time a vertex is used for a different side of our cube, you could remove some that are exactly the same, but in our next session we'll see there is a good reason for this.
I hope that the previous sections gave a bit of an overview behind the thinking of creating a 2D game. The stable of 2D games are sprites which are then placed on screen usually within a fixed grid and while we'll leave most of that behind when jumping into 3D it is still usable for things like the UI.
But it isn't just the rendering side that is interesting, when we look at how we made Conrad move throughout the 2D environment having him move from one tile to the next, and thus giving properties to the tiles in the form of our interaction map, we very much simplified the logic needed to enable this.
When we look at 3D games we often think about games with a lot of freedom and indeed many of the out of the box 3D engines focus on enabling this using relatively expensive collision detection and similar techniques to let a character interact with the 3D world.
But there are legions of games out there that take a simpler approach very much keeping the same logic for interacting with a 2D world but rendering the environment in 3D. Take many RTS games for instance which still play on a 2D map (often with a few levels giving more of a 3D feel to the maps), but simply render everything in 3D giving the player full control of the camera.
When we look at platform games, Flashback tells an interesting story as its successor, Fade to Black, was an early attempt to bring this kind of game into the 3D world with mixed success. Fast forward to today and we see many successful games in this genre that allow full 3D interactivity, Tomb Raider and Uncharted spring to mind.
But there also was a remake of Flashback that essentially kept the game dynamics the same as the original but rendered the environment in 3D.
Another title that comes to mind that made a similar jump is Oddworld. I had the pleasure of playing an altered version of the 3D version of Oddworld using stereoscopic rendering and it simply blew me away, after that I firmly believe the industry gave up on 3D stereoscopic gaming way to quickly.
The reason for this rant? Simple, for an indie game developer on a budget I believe that looking at developing games that internally are essentially 2D games but rendered to 3D is a worthwhile endeavor. This will have my main focus for the continuation of this series, to turn our little 2D platformer into a 3D platformer keeping much of the game dynamics the same.
But before we get there, we've got some boring ground to cover. For now I've gutted our little example and put the bare minimum in there to render a box in 3D. On purpose I'm leaving a few things out as to not put to much information in one write-up. This one will be too long as it stands.
Oh, I've also not removed some of the files that are now no longer in use so please ignore those.
3D projection
In our 2D tutorial we introduced the concept of orthographic projections which basically told OpenGL how to transform a vertex to the correct coordinates on the screen. This type of projection ignores the Z component of our vectors and simply scales and translates the X and Y for display on screen. The Z is still used for layering but does not influence where things are drawn.
In 3D this changes. The further away an object is, the smaller it should be drawn on screen. I won't go to much in-depth to the internals of this but in essence we will be using a matrix that divides the X and Y by our Z to create the illusion of depth (in the matrix itself the division actually happens through our W component which is derived from our Z, this so we retain our Z value which comes in handy in our rendering process). This matrix creates what is called a viewing frustrum which defines a 3D volume that confines what we are seeing on screen:
(images courtesy of wikipedia)
We can create this matrix using the function mat4Frustrum in our math3d.h library however there is a second helper function that is a little bit nicer called mat4Projection. This function takes a FOV (Field Of View) value and the aspect ratio of your display to calculate the right values for our mat4Frustrum.
We also need to define our near and far plane. When we look at the human eye near is very close and far is infinite but for a computer these values are important because they determine how our Z is scaled so we can use our depth buffer. The bigger the gap between near and far, the less precise our Z buffer becomes, the more chance things won't look right on screen as something that is behind another object could be drawn on top of it.
The flipside of this argument is that the human eye is focused on a particular distance and only objects near that distance are sharp and everything else gets blurry while the computer renders everything in focus (mimicking focus we might look into much further down as there are some neat tricks to make this work though I am not a big fan of it).
In our engineRender function in engine.c we can see this function used where we previously had our mat4Ortho call:
// set our model view projection matrix
ratio = (float) pWidth / (float) pHeight;
// init our projection matrix, we use a 3D projection matrix now
mat4Identity(&projection);
mat4Projection(&projection, 45.0, ratio, 1.0, 10000.0);
This sets a projection matrix with a 45 degree field of view.
As before in our 2D tutorial next to our projection matrix we need a view matrix which defines our "camera". At this point in time we'll leave this as an identity matrix, we'll discuss positioning a "camera" in a future part of this tutorial.
With this combination we now have our X axis going from left to right (positive X is to the right), our Y axis going from bottom to top (positive Y is pointing up) and our Z axis going from far to near (so we're looking into negative Z) with 0,0,0 being at the center of the screen.
So point 10, 20, -30 is position 10 units to the right, 20 units up and 30 units in front of our "camera".
Note that we do later on use our orthographic projection so we can draw our FPS counter and you would normally do this for other UI elements as well.
3D Models
Now that we have our projection all set up we need something to render to screen. For our first step we're going to keep this pretty boring and render a cube. This will however allow us to slowly introduce individual concepts and deal with individual problems as we enhance our example.
We construct our cube using two primitives, vertices which are shown as the green dots in the diagram above, and polygons which are solid shapes formed by connecting 3 or more vertices. While many 3D modeling packages allow any number of vertices that form a polygon OpenGL prefers the use of triangles and in fact since OpenGL 3 you can only use triangles for rendering.
In the fixed pipeline of OpenGL 1 the definition of our vertices was fixed but with the advent of programmable shaders we've now got a lot of freedom in how much information we record for each vertex. We call these attributes of our vertex and they can be things like:
- the position of the vertex in 3D space
- the normal for that vertex (we'll get back to this)
- one or more texture coordinates for that vertex (we'll also come back to this)
- the color for that vertex
The only attribute we're likely to always find is the position of the vertex. You have a lot of control over how this data is organized but for our example we're going to define a structure for our vertex and define an array. Our initial example will record the position and a color for that vertex.
// we define a structure for our vertices, for now we define the location and color of each of our vertices on our cube
typedef struct vertex {
vec3 V; // position of our vertex (XYZ)
vec3 C; // color of our vertex (RGB)
} vertex;
vertex vertices[] = {
-0.5, 0.5, 0.5, 1.0, 0.0, 0.0, // vertex 0
0.5, 0.5, 0.5, 0.0, 1.0, 0.0, // vertex 1
0.5, -0.5, 0.5, 0.0, 0.0, 1.0, // vertex 2
-0.5, -0.5, 0.5, 1.0, 0.0, 1.0, // vertex 3
0.5, 0.5, -0.5, 1.0, 1.0, 0.0, // vertex 4
-0.5, 0.5, -0.5, 0.0, 1.0, 1.0, // vertex 5
-0.5, -0.5, -0.5, 1.0, 1.0, 1.0, // vertex 6
0.5, -0.5, -0.5, 0.0, 0.0, 0.0, // vertex 7
};
The position (V) is a simple X,Y,Z coordinate, for the color (C) we also use a 3D vector which now contains an R,G,B value. In this way we store 8 vertices for our cube.
For our triangles we use a simple index list in which we store 3 entries for each triangle we render:
// and now define our indices that make up our triangles
GLint indices[] = {
0, 1, 2,
0, 2, 3,
...
4, 5, 6,
4, 6, 7,
};
Now the order of our 3 vertices that make up each triangle is very important. For any 3D model that forms a solid any 'back facing' polygon will always be obscured by a 'forward facing' polygon. On our cube it is never possible to have more then 3 sides visible at any given time. Now luckily there is a very easy check for this by looking at the "normal" vector of the polygon and seeing if this points towards or away from the camera. The "normal" vector of a polygon is a vector that stand perpendicular to the plane of the polygon. This is extremely easy to calculate for a triangle by simply calculating the cross product of two edges of the triangle and OpenGL is able to do this automatically for you by turning backface culling on:
The first command enables the culling logic. The second command tells OpenGL that if vertices are position in clockwise order they are front facing (the default is counter clockwise). The third command tells OpenGL to cull the 'back facing' polygons.
Now in the old days we would use the arrays we just defined (or loaded from disk, which we'll dive into in a later tutorial) directly but this would cause a lot of copying data from normal memory to our graphics card on each frame. Eventually OpenGL got smart enough to do this copy once and reuse the data loaded into the graphics card and in OpenGL 3 this now is the only way to go.
We've already used part of this in our previous examples but we're now going to use the full deal.
First we again need a Vertex Array Object or VAO. Here VAOs are beginning to shine as they will encapsulate all the data related to our model. With our one cube this isn't to special but if we have different objects all we need to do is bind the VAO for the model we wish to render and all state associated with that model is made current.
Second we need two Vertex Buffer Objects or VBOs that contain our actual data. We'll copy our vertices in the first VBO and our indices into our second VBO.
Last but not least we need to tell OpenGL about the two attributes our vertex VBO now contains. The whole code for loading our cube looks as follows:
void load_objects() {
// we start with creating our vertex array object
glGenVertexArrays(1, &VAO);
glGenBuffers(2, VBOs);
// select our VAO
glBindVertexArray(VAO);
// now load our vertices into our first VBO
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), &vertices, 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);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) sizeof(vec3));
// now we load our indices into our second VBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBOs[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices, 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);
};
Hopefully the comments are enough to explain the code. The tricky command in this is glVertexAttribPointer which tells OpenGL how the data loaded into our VBO is organized.
Note: as long as we wish to render this model we need to keep both the VAO and the two VBOs alive but if we had loaded the model from disk into temporary arrays we could free up this memory as we've copied it into memory managed by OpenGL. Freeing up our VAO and VBOs and thus freeing up the memory allocated by OpenGL is done in this function:
The final ingredient here is our shader but in this case it is deceptively simple. I didn't want to touch on the complex stuff just yet which is why I opted for adding a color attribute. The effect is fairly psychedelic as our shader simply interpolates the color value between each vertex as it draws the polygons.
Here however the difference between vertex shader and fragment shader becomes much more apparent. Our vertex shader is executed for each of our 8 vertices and at this point in time it has two goals:
calculate the screen coordinates of each vertex by applying our model-view-projection matrix
determine the color of each vertex
This is the code for our vertex shader:
#version 330
layout (location=0) in vec3 positions;
layout (location=1) in vec3 colors;
uniform mat4 mvp; // our model-view-projection matrix
out vec3 color;
void main(void) {
// load up our values
vec4 V = vec4(positions, 1.0);
color = colors;
// projection of V
gl_Position = mvp * V;
}
The first new thing we see are our attribute definitions, these map directly to our attribute definitions we declared when we loaded the data for our cube. Within our shader these variables will point directly to the entry in our array for which our vertex shader is being called.
We then define our color as a vec3 output variable that we can use in our fragment shader.
In our function we calculate V as a 4D vector for our position and simply copy our color.
We then apply our model-view-projection matrix (mvp) to V and store the result in gl_Position, one of the few build in OpenGL variables still supported in OpenGL 3.0.
Next OpenGL will use our indices to render the polygons taking the output of our vertex shader for each of the 3 vertices of our triangle and then interpolating our output variables. As we only output our color the color is nicely mixed over the surface of our cube. Our fragment shader is then called for each pixel we render out to screen.
As the interpolation is done before our fragment shader is called there is very little more to do and our fragment shader simply copies its input to our output:
#version 330
in vec3 color;
out vec4 fragcolor;
void main() {
fragcolor = vec4(color, 1.0);
}
This is where OpenGL gets a little silly. In the original shader implementation our fragment shader the output variable was a build in variable just like gl_Position but unlike gl_Position it was removed and instead our fragment shader must have a single output variable.
There is good reason for this however which we may come back to in a later tutorial. When we use techniques such as deferred rendering a fragment shader can have multiple outputs and this change starts making sense.
I've covered loading the shader into OpenGL in previous parts of this tutorial so I won't go over it again, the code can be found in the function load_shaders().
Rendering our cube
This is the bit where I really like VAOs. To render our cube we need to do 3 things:
calculate our model-view-projection matrix
select our shader
render our VAO
For rendering one cube this may not seem very special but imagine rendering dozens of things you can see how little work there is to do in rendering them if you've loaded them into VBOs and setup a VAO for each of the objects to render.
The code for this is:
// set our model view projection matrix
mat4Copy(&mvp, &projection);
mat4Multiply(&mvp, &view);
mat4Translate(&mvp, vec3Set(&tmpvector, 0.0, 0.0, -30.0)); // move it back so we can see it
mat4Rotate(&mvp, rotate, vec3Set(&tmpvector, 10.0, 5.0, 15.0)); // rotate our cube
mat4Scale(&mvp, vec3Set(&tmpvector, 10.0, 10.0, 10.0)); // make our cube 10x10x10 big
// select our shader
glUseProgram(program);
glUniformMatrix4fv(mvpId, 1, false, (const GLfloat *) mvp.m);
// now render our cube
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 12 * 3, GL_UNSIGNED_INT, 0);
For calculating our mvp I'm doing everything in one go instead of first creating a model matrix. The model matrix consists of 3 steps (they are in reverse order in the code):
Scale the cube to size
Rotate the cube based on an angle that we increase as time goes by
Move (translate) the cube into position
I've put the scale step in on purpose, I could have defined the cube at the correct size. We'll get back to that in due time.
Selecting the shader is a simple call to glUseProgram and then loading our mvp into our shader.
Rendering the VAO is now 2 simple steps: binding the VAO (which also binds the VBOs and calling glDrawElements to tell OpenGL to draw our triangles.
Now in this last bit we do see our first little hickup for those of you who are aware of how 3D modeling software often handles models especially when different materials are used with a single object. Often enough we'll have one array of vertices but then multiple arrays of indices which each need to be drawn using a different material (often different shaders, or the same shader with different settings). This mostly is a space saving issue as we can reuse vertices but as we'll later on find out this issue quickly becomes mute in a 3D engine as vertices will need to be duplicated for other reasons.
Now we can use the same VBO over multiple VAOs and thus reuse the same vertex array but personally I think it works much better in OpenGL if each VAO has its own vertex array and index array that renders (part of) an object with a single material.
In the next part we're going to make our cube a little less boring by applying a texture to it.
The part after that we'll discuss basic lighting.
After that we'll probably be ready to start applying the first part of the changes to our platform game.