Friday 22 January 2016

Loading models #2 (part 17)

Well it sometimes helps going places were there is no internet, I managed to write most of the model loading code over the weekend. I was originally planning on doing this in two more parts but here we go with an extra long write-up and it thus took me a little while longer before this was ready:)

First off, I made two structural changes to the mesh object that need to be highlighted.

The first one is that I added a retain count to the object. When you create a mesh object the retain count is set to 1. If you pass the pointer around and store the pointer in an array you can call meshRetain to increase the retain count.
When you no longer need to keep the pointer to the object you call meshRelease, this decreases the retain count. Once the retain count reaches 0 the memory is freed up.

The second change is that I changed the two arrays that hold the mesh data. I introduced a new support library called dynamicarray.h that manages an array that holds data. A very simple implementation where you would use std::vector in C++.

The release/retain approach I've started retrofitting to a number of structs where I found this handy.

Also before I forget, I've compiled GLFW 3.1.2 for Mac and removed the windows files until I get the change to compile GLFW for windows so please be aware of that if you're on windows.

New support libraries

I've added a number of new support libraries with rudimentary code to do some common tasks. I'll either enhance them as time goes by or replace them if I find existing C libraries that others have created that save me the trouble. The really cool ones I have are usually C++ libraries, alas.
I'll briefly discuss them here but point to the code within for further details as they are really out of the scope of what we're trying to learn here.

As with the other libraries, they are once again single file implementations.

errorlog.h

This needs very little explanation, I got tired of copying the pointer to the error log function around so I placed it in a library that I'm using everywhere. I kinda disliked forcing an error log on everyone who uses the libraries but it's pretty easy to gut it or replace it with your own internals.

I haven't retrofitted it into 2D libraries yet but will soon.

varchar.h

Standard C libraries work pretty well with a fixed sized string but for variable length strings there is a lot of remembering to reallocate buffers and whatnot. Here is a simple implementation of an object that resizes the buffer containing the string for you. While the structure does have a length variable to indicate how long the string is, the string is also zero terminated so it can be used with standard C string functions. It's basically a poor mans std::string implementation. It also uses a retain counter.

Basically only the variable text, which contains the string, should be accessed directly as a read only variable or things will start falling apart. Note that this can be NULL pointer if no string is loaded.

So far the following functions are available:
newVarchar allocates memory for a new string object returning a pointer and initializing the struct held within.
varcharRetain increases the retain count.
varcharRelease decreases the retain count and frees up the memory once retain count reaches zero.
varcharCmp compares our string with a zero terminated string similar in function to strcmp.
varcharPos tries to find the position of a zero terminated string within our string.
varcharMid returns a substring of our string as a new varchar object (which must be released!).
varcharAppend appends a string at the end of our string.
varcharTrim removes and spaces and tabs from the start and end of the string.

As time goes by I'll add further string functions that makes sense, this hardly even covers the basics but it was all I need for now.

dynamicarray.h

I mentioned this in my introduction, this is a simplified implementation of a dynamic array similar to std::vector. It can hold any type of data but seeing C doesn't understand templating we can only tell our logic how many bytes long each entry in our array is. It is up to us to properly cast this data.

As such you should treat all variables in our struct as private and not access them directly.

newDynArray allocates memory for a new array, you specify the size in bytes for each entry.
dynArrayFree frees up the memory held by the array. If the data you placed within contains pointers those are NOT freed.
dynArrayCheckSize increases the amount of memory allocated for the array if required, handy to give a starting size and prevent unnecessary calls to realloc.
dynArrayPush adds a new entry to our array, increases the size of memory allocated if required. The contents of the data is copied into the array.
dynArrayDataAtIndex returns a pointer to a location within the array so you can either extract or change the data for that entry.

Again there are a few basic functions still missing, like being able to remove an entry or merge two arrays but again, this is all I need for now. One that we'll add soonish is the ability to quick-sort our array.

linkedlist.h

This is maybe one of the more complex support libraries. A linked list is an array where each entry in the array is allocated separately and the list is formed by the entries pointing to each other like a chain.
The big advantage of this is that it is fairly inexpensive to make changes to rather large lists, there is no need to reallocate all the data.
The disadvantage is that it is harder to traverse the list, you can basically only loop from the first to the last, and from the last to the first entry (and this later only only because I've implemented both a next and a previous pointer). It can thus be expensive to find an entry somewhere in the middle of the list.

There are two structs here, one is a container for meta data about our list, the second is a struct allocated for each entry. The data in these structures should be treated as read only.

The most common code fragment you'll need is to loop through all the entries of a linked list and you do this as follows:
llistNode * node = myList->first;
while (node != NULL) {
  mystruct * object = (mystruct *) node->data;

  // do your thing here

  // next!
  node = node->next;
};

I've chosen to store a pointer to the data 'held' within the list instead of copying the data like we've done with our dynamic array. As a result I've made the implementation optionally aware of the retain and release functions of our of our objects. When a pointer to an object supporting retain/release is added to our linked list, the object is automatically retained. If we remove an entry, or get rid of the linked list all together, the relevant objects are released.

newLlist allocates memory for our new linked list object, you can optionally provide a retain and release function for your entries.
llistFree frees up the memory for our new linked list object, note that if a release function was provided when the linked list was created, the release function is called for every entry currently in the linked list.
llistAddTo adds an entry to our linked list. If a retain function was provided it is called.
llistRemove finds the entry that points to the specified object and removes it. Only if both a retain and release function was provided do we release the object.
llistMerge adds all entries from one list to another. If a retain function was specified the objects are also retained. The source list is not modified.
llistDataAtIndex returns the pointer to the object (data) added at this index in our linked list.

Wavefront


We're nearly ready to start enhancing our application to actually load 3D object data. There are many formats out there each with their strengths and weaknesses. The biggest issue I always run into, especially with more modern modeling software, is that those tools are designed for creating rendered animations with a focus on realism and not always best suited for use in games. Often the formats are bloated with functionality that gets in the way of what we're doing. Formats are usually designed to fit well within the functionality offered by the modeling software, not optimized to how the hardware can most easily render the data.

One thing I often find myself doing is creating loaders for easily available file formats but then writing the data back out in a format that matches the engine I've build.

The format I've chosen for our tutorial is an oldy but goldy file format created by Wavefront. There are two things that really attract me to this format:
1) it's text which makes it easily readable and easy to learn and understand,
2) it's very too the point, it just contains mesh data, not other stuff we don't need.

It has only got two drawbacks:
1) it allows polygons with any number of vertices. We thus need to split them into triangles which doesn't always give a nice result. We could off course load the polygons as patches and use a geometry shader, but that is a topic for a tutorial far in the future.
2) our positions, normals and texture coordinates are 3 separate arrays which each vertex of our polygons maintains separate indices for. As we now know OpenGL doesn't work this way and we thus need to rearrange the data. This can be time consuming. Also all objects contained within a single file share a single array (even though they are often split up) which again we don't do.

Now there really are two formats that we're dealing with. An .mtl file which describes the materials used, and the .obj file which contains the actual mesh data.

We're going to look at the first one first but as we didn't have materials yet in our system we need to design those first.

Owh, and before I forget, for our example today I managed to get my hands on a Star Wars Tie-Bomber 3D model modeled by someone named Glenn Campbell. The model was downloaded from this great site called TF3DM. This is like a marketplace for 3d models with loads of free models and paid models to chose from. Many of the free models are only for non-commercial use but that suits us just fine. I chose the tie-bomber mostly because it was relatively small.
The only downside is that it doesn't use any texture mapping but as we've already applied texture mapping to our globe it's not a big miss. I have added (but not tested) all the code to load texture maps and texture coordinates for our meshes. I could be mixing up with 3DS files but it may be the texture will have ended up upside down:)

Materials


I've consciously kept materials apart from the shaders that actually bring those materials to life. A material describes the properties of the materials that forms our 3D objects, the shader then uses that to draw it appropriately. All the logic is contained in a new library called materials.h

Now we do need to talk about the shaders first.
You'll see that I've now got 3 separate shaders. Our texture based shader we had before, a single color shader and a reflection shader (more on this later). I could have written a single shader with if statements but due to the way graphic cards work even the "false" condition can end up being executed and its results discarded if the parallel processing deems it can use some idle core to do that work on the assumption the condition may be true. If we do expensive lookups that 9 out of 10 times are not needed, we're just burning GPU cycles for nothing.
The effects can however be combined so in theory we need a large combination of shaders to do our rendering.
This is something we'll look into at some later stage as it's a topic on its own but our approach will remain similar to what we're doing now. For each material we'll chose the best shader for the job. Right now we do that in our render loop but we may move that to an initialization stage at some point.

The focus thus lies on a structure that retains information for a particular material:
// structure for our material info
typedef struct material {
  unsigned int  retainCount;      // retain count for this object
  char          name[50];         // name of our material
  
  GLfloat       alpha;            // alpha for our material
  vec3          matColor;         // diffuse color for this material
  vec3          matSpecColor;     // specular color for this material
  GLfloat       shininess;        // shininess of this material
  GLuint        textureId;        // id of our texturemap
  GLuint        reflectId;        // id of our reflectionmap
} material;
This structure may grow as we keep adding functionality to our rendering logic but it has all the basics.
Most of the variables in our struct can be changed directly but some, like our texture maps, must be set through the appropriate functions.

Because a material may be shared amongst multiple models and we want to properly manage the memory usage we again use a retain count for this object.

We've then implemented the following methods:
newMaterial allocates memory and initializes a new material.
newMatList returns an empty linked list object properly configured to store material objects into.
matRetain increases the retain count on our object.
matRelease decreases the retain count on our object and frees up memory used once the count reaches zero.
getMatByName searches a linked list containing materials for a material with the given name. Note that if you want to hang on to that material you must retain it!
matLoadTexture loads the specified image file as the texture map for our material.
matLoadReflect loads the specified image file as the reflection map for our material.
matParseMtl loads the materials defined in the specified wavefront mtl data and adds them to a linked list.

This last function, matParseMtl is where the magic happens. Just like with our shaders I've kept loading the material file separate and we thus provide already loaded data to our function.
We also use a materials linked list to store our materials in. There is no protection to ensure a material with the same name doesn't appear more then once in our materials list, we assume we're taking care with loading files for now. We load our material data as follows:
  // create a retainer for materials
  materials = newMatList();
  ...
  // load our mtl file
  text = loadFile("tie-bomber.mtl");
  if (text != NULL) {
    matParseMtl(text, materials);
      
    free(text);
  };
  ...
I'm not going to go through the entire code for loading these files but the parser is fairly simple. We extract each line of the file one by one and place it in a varchar object. We skip any empty lines or lines starting with a # as this is a comment.
We then make sure we trim any unwanted spaces and then find the first space in the string. The text returned here identifies what data we're loading at.
Each new material always starts with a line starting with newmtl and we create a new material object each time we encounter it.
I've added comments for each other type of line that we encounter and currently parse but it should be pretty self explanatory.

Object file


Now that we've loaded our material information we can load our object file. The object file tells us which material file it uses but I'm ignoring this as we've already loaded it.

We've taken the same approach of loading the data into a buffer first and then calling the function meshParseObj that has been added to our mesh3d.h file.

A single wavefront object file can contain any number of "objects" which are subdivided into "groups". Think of this as the objects being cars, bikes, street lamps, etc, while the groups subdivide an object such as a car into the body, wheels, windscreen, etc.
I'm currently ignoring the objects taking a "single file is single object" approach as this requires a layer on top of our mesh objects which we have not yet build.
Just like with our materials we provide a linked list containing mesh3d objects to which we'll add the object we load. We also provide our materials linked list so we can look up materials and assign them to our mesh3d objects.

An important rule is that each mesh3d object is rendered using a single material. So our mesh3d objects really fit well with the definition of groups in the wavefront files.

While a bit crude, and we do support it, both objects and groups are optional and if not specified we should assume a default object is required.

Just like with materials, there is no problem in adding two mesh3d objects into our list that happen to have the same name.

Parsing the object file is remarkably similar to parsing the materials file, it is no surprise the structure of the files are relatively the same. We do see that as we're loading position vertices, normals and texture coordinates we are loading those into temporary arrays.
Then as we're loading faces we're keeping track of combinations of these 3 units for which we've added vertices to our mesh3d object. Especially on large wavefront files this can churn through a fair amount of memory and isn't implemented particularly fast but as I mentioned before for a real production application I would implement some sort of conversion tool to export the data into a format more closely mimicking my 3D engine.

The source is too bulky to present here so I again refer to the comments I've made in the source.

Changes to shaders


Next I needed to enhance the shader implementation. Our reflection shader is the first that works in world space so I needed to ensure my shader had a normal matrix without our view matrix applied and I needed to communicate the location of our eye.

I also decided to add in a number of other standard IDs likely used for our materials to our shaderStdInfo struct and ensure those IDs got setup correctly. I also added our light position but in this case it's a temporary fix. More about that much later in the series.

I've also introduced a structure that simple holds our main 3 matrices (perspective, view and model matrices) called shaderMatrices so it is easy to pass this to our shaderSelectProgram function.
I also introduced a structure called lightSource that holds both our world position of our light and the position adjusted by our view matrix.

But the biggest addition is sending our material to our shaderSelectProgram function to communicate the properties of our material.

This will do fine for what we're doing so far. The biggest issue we're having right now with our shader is that each time we render a mesh, we initialize the entire shader. This is something we'll have to work on as our project becomes more complex. Again however, a topic for later.

As mentioned earlier, we now have our textured shader, a single color shader where instead of getting the color from our texture map we just set our color we now also have a reflection shader. Soon I'll write one session in my tutorial on better organizing our shaders but this split will do us fine for now.

The reflection shader, also often called environment shader, is an old trick that can be very useful. I included it because our model had a number of maps setup this way.

The very last material in our material file, CHROME_GIFMAP probably showcases its use the best as a method for rendering shiny metal. Unfortunately there are only two small parts in the model that use this. The body of the tie-bomber also has a reflection map but the map provided with the model seemed to be a template. I think the original idea was to reflect stars. I've added a starmap but it's far to bright making the effect look very fake but in doing so it serves as a pointer to the limits of this technique.

The technique is very simple, you take the vector from your eye to where it hits the surface, then use our normal to reflect that vector to see where the light that is reflected comes from. You then use a texture map to look up the color. It works amazingly well on curved surfaces but less on flat surfaces.
It is often used to simulate metal as this works well with very blurry maps that don't reveal to much detail and give the effect away. It is also a great way to simulate ambient light by taking an actual spherical map of the surroundings, blur the image and multiply with the ambient color of the material and voila. Another use is for reflections on a shiny or wet floor or on the surface of the water where again the reflection is distorted and you can get away with using a static texture.

But as a true reflective surface like a mirror, this technique often falls short especially on flat surfaces. You'll quickly realize the reflection isn't related to whats around you. But luckily there are better solutions to that which we'll undoubtedly get to in due time.

Changes to render loop


And now on to our last step, actually rendering our whole tie-bomber. Our fixed two meshes we were rendering has now been replaced by a loop that loops through our linked list and renders each mesh individually:
  if (meshes != NULL) {
    int count = 0;
    llistNode * node = meshes->first;
    while (node != NULL) {
      mesh3d * mesh = (mesh3d *) node->data;
      if (mesh == NULL) {
        // skip
      } else if (!mesh->visible) {
        // skip
      } else {
        bool doRender = true;
        // set our model matrix, this calculation will be more complex in due time
        mat4Copy(&matrices.model, &mesh->defModel);

        // select our shader, for now we have 3 shaders that do different types of shading
        // eventually we'll add something that allows us to make combinations
        if (mesh->material == NULL) {
          shaderSelectProgram(&colorShader, &matrices, &sun, NULL);
        } else if (mesh->material->alpha != 1.0) {
          // postpone
          alphaMesh aMesh;
          GLfloat z = 0; // we should calculate this by taking our modelview matrix and taking our Z offset
        
          // copy our mesh info
          aMesh.mesh = mesh;
          mat4Copy(&aMesh.model, &matrices.model);
          aMesh.z = z;
        
          // and push it...
          dynArrayPush(meshesWithAlpha, &aMesh); // this copies our structure
          doRender = false; // we're postponing...
        } else if (mesh->material->reflectId != MAT_NO_TEXTURE) {          
          shaderSelectProgram(&reflectShader, &matrices, &sun, mesh->material);          
        } else if (mesh->material->textureId != MAT_NO_TEXTURE) {          
          shaderSelectProgram(&texturedShader, &matrices, &sun, mesh->material);          
        } else {
          shaderSelectProgram(&colorShader, &matrices, &sun, mesh->material);
        };

        if (doRender) {
          // and render it, if we fail we won't render it again
          mesh->visible = meshRender(mesh);          
        };
      };
    
      node = node->next;
      count++;
    };
  };
Chosing which shader to use is a bit simplistic but fine for where we are now however I'm making an exception for any material that isn't fully opaque but has a level of transparency.

Opaque materials are easy, the Z-buffer will sort things out, but with transparent materials we need to know the underlying color and blend. Also this blending process is relatively expensive as it requires reads from our output buffer so we gather our transparent meshes in a list, and then postpone rendering them till later:
  // now render our alpha meshes
  if (meshesWithAlpha->numEntries > 0) {
    // we should sort meshesWithAlpha by Z
  
    // we blend our colors here...
    glEnable(GL_BLEND);
    glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    for (int i = 0; i < meshesWithAlpha->numEntries; i++) {
      alphaMesh * aMesh = dynArrayDataAtIndex(meshesWithAlpha, i);
    
      mat4Copy(&matrices.model, &aMesh->model);
      if (aMesh->mesh->material->reflectId != MAT_NO_TEXTURE) {          
        shaderSelectProgram(&reflectShader, &matrices, &sun, aMesh->mesh->material);          
      } else if (aMesh->mesh->material->textureId != MAT_NO_TEXTURE) {          
        shaderSelectProgram(&texturedShader, &matrices, &sun, aMesh->mesh->material);          
      } else {
        shaderSelectProgram(&colorShader, &matrices, &sun, aMesh->mesh->material);
      };

      aMesh->mesh->visible = meshRender(aMesh->mesh);
    };    
  };
The above code still has various holes in it but I wanted to keep things as simple as I could. As mentioned in the comments we should sort this list by distance, to render transparent meshes that are distant first.

But that is something that we'll be changing in the entire loop. Our initial loop will eventually result in two dynamic arrays, one with opaque meshes, one with transparent meshes, which subsequently are rendered one after the other. I'm getting ahead of myself but this approach has a number of reasons:
  1. meshes will have relationships to each other, a wheel of a car will have a model matrix that positions it relative to the center of the car. When rendering the car we take the model matrix of the car to position the car, but we combine the model matrix of the car with the model matrix of the wheel to position the wheel
  2. meshes may be off screen. We can exclude many models that are part of our world but currently not on screen from the rendering process.
  3. we can sort our opaque meshes in a way that makes best use of our shaders and as mentioned before sort our transparent meshes by distance.
I'm sure I'm leaving some other important ones out.

Well after all this work, we have our tie-bomber....

As you can see it isn't without fault. One problem that isn't apparent in the video is that the model isn't properly centered. This is something I'll need to deal with in the model loading code in due time.
The other issue is that the dark indented areas on the 'wings' aren't fully rendered on one side of the model, at this point I'm not sure what the cause is.

Download the source here

What's next


Well unfortunately I'll likely be taking a bit of a break. I might throw in a few blog posts on several subjects such as shaders and other smaller improvements I'll be making but the next step will be to pick up our platform game again and start porting it to 3D.

The biggest hurdle at this point is that it requires a lot of non-programming work before I can actually build the next step. Copying and pasting images into a tilemap is easy, creating 3D models takes a lot of time. But we'll get there in the long run :)

No comments:

Post a Comment