Sunday 7 February 2016

Adding a skybox (part 20)

Okay, I'm back with an other installment. Before we get to our skybox, I've starting cleaning up some code and polishing a few things up so lets look at that first.

A new folder structure


One thing that was really starting to annoy me was dumping all the files into our resources folder and together with our .exe on Windows. One thing that really makes this a pain is that we are dealing with different path delimiters on each platform. I'm not entirely happy with how I solved things yet but at least we've now got a bit more of a manageable structure. Fonts go into Fonts, Model files into Models, Textures into Textures and Shaders into Shaders.

Also on Windows I decided to just create a Resources folder to put everything into. I haven't tested the Windows build yet so expect some typos or things I've forgotten to change in the makefile.

For Windows I'll eventually change the working folder to the Resources folder so we can make it work in line with our Mac code and maybe look into a path parser that replaces path delimiters for the correct platform. We'll see.

Materials and shaders


I've made a start in better structuring the materials and how they use shaders. It is now the material that is leading and sets up our information OpenGL needs to render the material and the shader to use is simply a property of the material now. That means that if we don't know what material to use for an object we simply use a default material we've added to our material list at the start (obviously none should be missing).

For now there is a little loop in load_objects that assigns the shaders to our materials:
  // assign shaders to our materials
  node = materials->first;
  while (node != NULL) {
    mat = (material * ) node->data;

    if (mat->reflectMap != NULL) {  
      mat->shader = &reflectShader;
    } else if (mat->diffuseMap != NULL) {          
      mat->shader = &texturedShader;
    } else {
      mat->shader = &colorShader;
    };
    
    node = node->next;
  };
This is a bit of a place holder for the time being, this still needs to change to something better.

Texturemaps


One thing that was a bit of an eyesore was that I was loading the same texture maps multiple times in my material loader as they were reused for different materials.
I've given the texture map loading code it's own place in a new library called "texturemap.h".

It works along the same line as all the other support libraries we've created so far but has one new trick up its sleeve. It maintains a linked list of all texture maps currently loaded when texture maps are loaded by calling "getTextureMapByFileName".
As we do not know whether we are getting a texture map returned that was already loaded or a new texture map the rule is that if the function calling this function wishes to retain the texture map, it needs to do so.

In this I'm following a rule that that comes from objective-C (where I first encountered retain/release) that a function that begins with "new" returns an already retained object and that a function that begins with "get" returns an object that needs to be retained.
We've got no need for autoreleasing objects just yet and I'm hoping we won't but I'm starting to wonder if I shouldn't create some sort of base library for my retain/release approach. Another day...

When we look at material.h we can see that we no longer use texture id's directly but go through our texturemap object. We can also see that we no longer have our load functions but instead have the functions "matSetDiffuseMap" and "matSetReflectMap" that set a pointer to our texturemap object.
If you look at the code for these two methods you again see a trick that stems from obj-C's property structure where we nicely release our old map and retain our new one.

As a result we can do something like this (as we indeed do for our skybox):
matSetDiffuseMap(mat, getTextureMapByFileName("skybox.png", GL_LINEAR, GL_CLAMP_TO_EDGE));
Our getTextureMapByFileName function checks i we've already loaded our map and if not loads it, our matSetDiffuseMap assigns it to our material and retains it. If loading fails a NULL pointer is returned by getTextureMapByFileName but matSetDiffuseMap deals with this just fine.

While all our materials will release their texture maps as they get destroyed it is important to remember our cache still retains all the loaded textures. We must not forget to unload this as well as we clean up at the end:
void unload_objects() {
  // free our object data
  tmapReleaseCachedTextureMaps();
  ...

Our skybox


So first off, what is a skybox? This is a technique often applied to create our far away background, say the map of the sky (what's in a name right?). By projecting our background on a very large box for which we're on the inside we create a background that automatically adjusts itself as we move our camera around.

Seeing we've got a tie-bomber a sensible skybox would be a star scape but I wanted something that shows off the technique a little better. Doing a bit of googling there are hundreds if not thousands of graphics available if you're not able to make one yourself. I came across RB Whitaker's site who's written up a lot of interesting graphics things but using C# and XNA. He also has a some graphics he's kindly donated to the public domain so there you go.

We often see the texture maps laid out in a T shape to minimize the number of borders that don't line up nicely. This is important because we interpolate our textures when rendering. I've however gone for a 2x3 layout to minimize wasting space in my texture but we have to keep in mind that we can't move our textures up to each border as we get some strange artifacts on the edges.


You'll see in the code where we generate our cube mesh I've left a 2 pixel border around each image.
The image itself show the front, back, left, right and finally top and bottom images in 3 rows. They are simply mapped on a box or cube.

Three more things set our skybox apart from rendering other objects.
First is that it's inside out. Since we are inside of our cube we need to see it's insides.
Two is that we don't want to apply any lighting but render the cube as is. We've thus got our own shader in the form of the shader programs "skybox.vs" and "skybox.fs". The code should be easy to read by now.
The third is that we're not using our model matrix but always center our skybox on the user. The idea is that the background is infinitely away so it only moves as you look around.

Infinity itself isn't really possible so the size of our skybox is dictated by our far plane. Now our far plane in our example stands at 10,000.0, our box is also 10,000 wide, high and deep. The observant amongst you will thus say that it's only going to be at a distance of 5,000 and that would be true, but as the box rotates the furthest point would be just over 8,500 (the furthest point is at 5000,5000,5000, calculate the distance to that point using pythagoras theorem).

This is one reason why some use a sphere as a "skybox" but a sphere is harder to texture without getting artifacts at the poles.  The principles stay the same.

The code for adding the skybox can be found in load_objects:
  ...
  // add the skybox last so it renders at the end
  mat = newMaterial("skybox");                // create a material for our skybox
  mat->shader = &skyboxShader;                 // use our skybox shader, this will cause our lighting and positioning to be ignored!!!
  matSetDiffuseMap(mat, getTextureMapByFileName("skybox.png", GL_LINEAR, GL_CLAMP_TO_EDGE)); // load our texture map (courtesy of http://rbwhitaker.wikidot.com/texture-library)
  mesh = newMesh(24, 36);                     // init our cube with enough space for our buffers
  strcpy(mesh->name,"skybox");                // set name to cube
  meshSetMaterial(mesh, mat);                 // assign our material
  matRelease(mat);                            // and release it, our mesh now owns it
  meshMakeCube(mesh, 10000.0, 10000.0, 10000.0, true);  // create our cube, we make it as large as possible
  meshFlipFaces(mesh);                        // turn the mesh inside out
  meshCopyToGL(mesh, true);                   // copy our cube data to the GPU
  llistAddTo(meshes, mesh);                   // add it to our list
  meshRelease(mesh);                          // and release it, our list now owns it
  ...
Note that small changes were added to our cube generation code to handle the correct texture mapping.

As the comment mentions we also add our skybox last. It can even make sense to leave it our of our render buffer and just render it separately at the end of our render loop. While the parallel processing on our GPU may result in more being rendered then we expect the skybox covers the entire screen while at the end of our render loop many object will already obscure it. Rendering it last does increase the chance of our Z-buffer discarding many pixels. Still the shader is simple enough for this to potentially not have much of an impact.

And here is the end result (sorry no movie):




And yes yes, before anyone complains, obviously I'm still using the star scape as a reflection map isn't the proper thing to do. I simply haven't had the time to create a reflection map out of our skybox. That might be a nice thing to actually automate, I'll give that some thought as that in itself would be a nice topic for a tutorial.

One last thing, I did try and see how this came out in stereoscopic mode and I was pretty happy. I have no idea if it will keep holding up if we find a need to push the far plane further out.

Download the source here

What's next?


As I mentioned in my last write-up, I'll be working on a separate project for a bit and as a result enhancing our little engine as I go. Hopefully as a result I'll be able to write up some more sessions like this one as I add more things.

One that is nearly ready to go and an important step forward is to not use our meshes directly but indirectly through instances of those meshes and these will be held within a 'tree'.
This will allow us to render the same object many times while positioned at different locations.
So next up, we'll change our single tie-bomber to a fleet of tie-bombers :)




1 comment:

  1. Casino Review - Dr.CMD
    If you're looking to win on 영주 출장샵 the slots or table 순천 출장마사지 games, 당진 출장샵 the 광주광역 출장마사지 slots at casino sites will likely be well suited for you. However, with the games on 서울특별 출장안마 table games

    ReplyDelete