Monday 15 February 2016

Instantiating objects (part 21)

I actually wrote this code some weeks back but didn't have the time to put it into my little tutorial project until this weekend. Slowly our little 3D engine is taking shape.

Housekeeping


As always there are a few fixes for things I found I had done wrong. One was setting the eye coordinate for our reflection mapping which required us to first calculate the inverse of our view matrix instead of using the view matrix directly.
The other was a dump typo in the code that applies a 4x4 matrix to a 3D vector. It initialized W as 0.0 instead of 1.0. The result is that our translation isn't applied.

I also made a few small enhancements to our mesh3d code.
First off, if you do not explicitly call meshCopyToGL it is automatically called the first time you try to render the mesh. It will free up our CPU buffers so if you wish to keep those, call meshCopyToGL before rendering.
I also added an adjust matrix to our loading code. Our tie-bomber for some reason was way off center and allowing us to adjust all the vertices of our object as part of loading it gives us a way to fix this.
There is also a new method called MeshCenter which finds the center of a single mesh, adjust all the vertices to properly center the mesh and then updates its model matrix accordingly. This can be handy but in the case of our tie-bomber it would still have the wrong end result so it is not used in our example.

A new library, meshnode.h


The big change is the introduction of a new library called meshnode. This library implements a "tree" in which to place everything we wish to render on screen. Nodes within this tree can (re)use the same mesh but apply a different model matrix to position the model somewhere else.
Nodes can also have child nodes. Here the model matrix of the parent node applies to all child nodes. For our tie-bomber this is nice because it allows us to move the entire model with all its individual pieces as one unit but it will also allow us at some later stage to move parts of a model in relation to itself. Picture a model of a car where you rotate the wheels around their own axis to simulate steering or animate the car driving.
Also the node structure allows us to show or hide entire models in one go.
This structure forms the basis of a lot of techniques that allow us to improve the versatility and performance of our engine and we'll get to those as time goes by.

The other mayor change this brings on is that we no longer handle the core render logic in our engine_update function. Instead the bulk is moved into the meshNodeRender method that is part of our node system.

You will notice way at the start of our engine.c file that we have replaced our meshes array with a single node called scene:
meshNode *    scene = NULL;
Ignore for a moment that we define an array of nodes on the next line, I'll come back to that in a minute.

Our scene node is the main container node into which we load everything that we render. This can be completely self contained and at the end of loading our meshes and organizing everything we'll end up with the following:
* scene
  * tie-bomber-0
    * TBSOLAR_01 => renders mesh3d TBSOLAR_01
    * TBWING_L01 => renders mesh3d TBWING_L01
    * ...
    * TBTOP_BO02 => renders mesh3d TBTOP_BO02
  * tie-bomber-1
    * TBSOLAR_01 => renders mesh3d TBSOLAR_01
    * TBWING_L01 => renders mesh3d TBWING_L01
    * ...
    * TBTOP_BO02 => renders mesh3d TBTOP_BO02
  * ...
  * tie-bomber-9
    * TBSOLAR_01 => renders mesh3d TBSOLAR_01
    * TBWING_L01 => renders mesh3d TBWING_L01
    * ...
    * TBTOP_BO02 => renders mesh3d TBTOP_BO02
  * skybox => renders mesh3d skybox

So we've included our tie-bomber 10 times in our tree and will thus end up rendering our tie-bomber 10 times, each time at a different location. The location is set on the tie-bomber-n node and applies to all its child nodes automatically by multiplying the parent nodes matrix with each child node.

As mentioned I also keep an array called tieNodes which is defined right after our scene node. These pointers point to our individual nodes within our scene. We could do without at the present time but looking forward doing this is very handy as it allows us to move our tie-bombers around without having to go search for our nodes within our tree. Easy at this point in time as we have only 3 layers but as we go on our tree may become far more complex.

After much internal debate I decided against changing my wavefront obj loader, it still returns an array of meshes as before. It was tempting to change the logic so it returned a single node with all the meshes loaded as child nodes. We could even go as far as to make it create a 3 level tree by taking the object and face group structure as separate layers.
While this makes sense I felt it over complicated matters and I wasn't going to go down the route of having my entire scene held within a single obj file as it would defy our goal of drawing multiple instances of the same meshes.
Instead a single wavefront file should contain one object like a tie-bomber, or a car, or anything else we might like and we composite our scene by some other means (currently in code).

Setting up our scene


So let's have a look at our load_objects() function bit by bit to see how we're using our meshNode library.

First off, I've moved all material loading to the start of this function. I was even tempted to give this its own function but for now I'm happy.

After our materials are loaded we create our scene object:
  // create our root node
  scene = newMeshNode("scene");
  if (scene != NULL) {
Now that was easy and pretty self explanatory.

Next up we load our tie-bomber file:
    // load our tie-bomber obj file
    text = loadFile(modelPath, "tie-bomber.obj");
    if (text != NULL) {
      llist *       meshes = newMeshList();

      // setup our adjustment matrix to center our object
      mat4Identity(&adjust);
      mat4Translate(&adjust, vec3Set(&tmpvector, 250.0, -100.0, 100.0));

      // parse our object file
      meshParseObj(text, meshes, materials, &adjust);
Now this is pretty similar to what we were doing before. The main differences are that we are creating our meshes linked list locally and that we introduce our adjust matrix which will end up setting a proper center for our tie-bomber
      // add our tie bomber mesh to our containing node
      tieNodes[0] = newMeshNode("tie-bomber-0");
      meshNodeAddChildren(tieNodes[0], meshes);
This is our first new bit, here we create a new mesh node for our tie-bomber and then call meshNodeAddChildren to add child nodes for each of the mesh3d objects held within our meshes linked list.
      // and add it to our scene, note that we could free up our tie-bomber node here as it is references by our scene
      // but we keep it so we can interact with them.
      meshNodeAddChild(scene, tieNodes[0]);
Next we add our node containing our tie-bomber to our scene. We could now release our tie-node as it is retained within our scene node but as I mentioned before we keep our array so we can more easily access our tie-bomber node in our update code (something for later).
      // and free up what we no longer need
      llistFree(meshes);
      free(text);
    };
That said, we do release our meshes linked list as our mesh3d objects are retained by the child nodes of our tie-bomber node.

At this point in time we have only one tie-bomber that we draw, to draw our other 9 tie-bombers we simply need to create new nodes that point to the same meshes. For this we have a handy little method called newCopyMeshNode. This makes a copy of a node. By default we reuse our child nodes but it has a deep copy option that would copy all the child nodes as well (but still reuse the same meshes). You would deep copy the nodes if you want to animate individual parts of your objects. Take our car as an example again, if we didn't copy all the child nodes changing the orientation of a wheel would apply that change to all instances of our object. For our tie-bomber however it'll do just fine.
    tieNodes[1] = newCopyMeshNode("tie-bomber-1", tieNodes[0], false);
    mat4Translate(&tieNodes[1]->position, vec3Set(&tmpvector, -400.0, 0.0, -100.0));
    meshNodeAddChild(scene, tieNodes[1]);
Here we see it in action, we make a copy of our tieNodes[0] node, then we change the position matrix of our new node so our tie-bomber moves to a new location, and we add our new node to our scene.

Now we repeat this for our other 8 tie-bombers.

Finally at the end of our load_objects function we add our skybox. I have moved this code into a separate function but it's basically the same code as in our previous example but with the extra step that we add a node to our scene for our skybox mesh.

Rendering our scene

Now that everything that we wish to render is contained within our scene node we can simply call the function meshNodeRender to render everything to screen:
  // init our projection matrix, we use a 3D projection matrix now
  mat4Identity(&matrices.projection);
  // distance between eyes is on average 6.5 cm, this should be setable
  mat4Stereo(&matrices.projection, 45.0, pRatio, 1.0, 10000.0, 6.5, 200.0, pMode);
  
  // copy our view matrix into our state
  mat4Copy(&matrices.view, &view);
  
  if (scene != NULL) {
    // and render our scene
    meshNodeRender(scene, matrices, (material *) materials->first->data, &sun);    
  };
Now that became a lot simpler:)

Obviously the code that used to be in our engine_update function has mostly moved to our meshNodeRender function but there is a key difference. In our original implementation we rendered opaque meshes directly while we placed meshes with a transparent material into an array.
Our new function builds two arrays, one for opaque meshes and one for transparent ones. Then they are rendered.

At this stage there is little added benefit to this other then simplifying our meshNodeBuildRenderList function. This function recursively 'walks' through our tree of nodes and calculates all the model matrices that are required to render our meshes.

However we now have the base for starting to add optimizations such as sorting this list to minimize switching shaders, minimize changing texture maps, minimize switch VAOs, etc. Again, stuff we'll start implementing in later parts of this series but we've done the ground work now.

The end result: we have 10 tie-bombers in a nice formation:

Download the source here

What's next?

I'm working on a simple height field implementation to add a group surface to the project I'm working on, I'll probably add that to our tutorial in the next session.

After that I finally want to make the jump to deferred rendering so we can start making the lighting a bit more fun.


No comments:

Post a Comment