Sunday 20 March 2016

LOD3 - bill board (part 24 continued)

Okay, I've split this last bit into a separate post. The bill board part of it isn't as much the issue, but the way we're going to generate our texture is and the previous post was simply getting too long.

So let's first have a look at what the bill-board technique is all about. We're actually already using it's smaller cousin when rendering our trees. The idea with bill-boarding is that when objects are small enough, or far enough in the background, rendering an entire mesh is overkill, rendering a textured square with an image of the mesh is all that is needed.

For small objects our leaves are a good example, our biggest tree has 100 leaves and that was only limited to 100 because the sapling plugin didn't allow me to add more. I guess the idea is that you would use a whole branch as a texture but even for that the principle stays the same. Generally speaking you would never look close enough to a leaf to see it's just a flat textured square, having a complex shaped leaf mesh just strains the GPU for something that you'd never look at, especially when rendering hundreds of leaves.

But bill boarding really comes into its own when we look at distant objects. When we look at our forest, we'll have many trees in the background very often obscured by other trees. You'd never notice they are just textured squares, by the time you get close we're rendering the actual mesh.
Even at our lowest level of detail our trees have many hundreds of faces, why render them all if a textured square will do?

Another good example where this is often used is in cities. Buildings close to the viewer are rendered in detail, buildings in the background are just textured squares.

Another example would be grass where often we add animating the squares for added realism.

When we look at grass, our leaves and buildings the squares we render are often fixed in place. For buildings it is even common to use a cube because buildings are large enough, and featureless enough to make them look weird if they move in unnatural ways.

But for our trees we're going one step further, we are going to alter our shader to make our square always face our camera on the horizontal plane. This might seem strange because this would cause our tree to rotate along with our camera and trees don't do this, but when the tree is far enough in the distance the rotation isn't noticeable unless you go looking for it.

A fun anecdote here is playing Far Cry 4 on my PS3 which uses a version of this technique. With the added helicopter (boy that thing is fun!) you can actually fly high enough above the trees and because you're looking down on them, see they are bill boards that adjust to the camera. On better hardware you can see they keep rendering proper meshes much further in the distance. Having your engine adapt the distances for your LOD switches depending on the hardware you're running your engine on is thus not a bad thing to do.

Ok, to make a long story short, the bill-board itself for our third level of detail isn't hard at all. We simply generate a mesh which contains a textured square or rectangle:
    // create our texture
    tmap = newTextureMap("treeLod3");

    ...

    // create our material
    mat = newMaterial("treeLod3");
    matSetShader(mat, billboardShader);
    matSetDiffuseMap(mat, tmap);
    mat->shininess = 0.0;

    // create our mesh
    mesh = newMesh(4,2);
    meshSetMaterial(mesh, mat);
    vec3Set(&normal, 0.0, 0.0, 1.0);
    meshAddVNT(mesh, vec3Set(&tmpvector, -500.0, 1000.0, 0.0), &normal, vec2Set(&t, 0.0, 0.0));
    meshAddVNT(mesh, vec3Set(&tmpvector,  500.0, 1000.0, 0.0), &normal, vec2Set(&t, 1.0, 0.0));
    meshAddVNT(mesh, vec3Set(&tmpvector,  500.0,    0.0, 0.0), &normal, vec2Set(&t, 1.0, 1.0));
    meshAddVNT(mesh, vec3Set(&tmpvector, -500.0,    0.0, 0.0), &normal, vec2Set(&t, 0.0, 1.0));
    meshAddFace(mesh, 0, 1, 2);
    meshAddFace(mesh, 0, 2, 3);
    meshCopyToGL(mesh, true);

    treeLod3 = newMeshNode("treeLod3");
    meshNodeSetMesh(treeLod3, mesh);

    // cleanup
    matRelease(mat);
    meshRelease(mesh);
    tmapRelease(tmap);
Here we see we create a new material using our texture map, then create a new mesh with 4 vertices and two faces, and set up our 3rd LOD node.

This would however create a mesh that isn't oriented by the camera but just placed in a plane oriented by our instance node. For this trick we've used a separate shader. This shader uses our texture fragment shader but has a slightly simplified vertex shader:
#version 330

layout (location=0) in vec3 positions;
layout (location=1) in vec3 normals;
layout (location=2) in vec2 texcoords;

uniform mat4      projection;     // our projection matrix
uniform mat4      modelView;      // our model-view matrix
uniform mat3      normalView;     // our normalView matrix

// these are in view
out vec4          V;              // position of fragment after modelView matrix was applied
out vec3          Nv;             // normal for our fragment with our normalView matrix applied
out vec2          T;              // coordinates for this fragment within our texture map

void main(void) {
  // load up our values
  V = vec4(positions, 1.0);
  vec3 N = normals;
  T = texcoords;

  // we reset part of our rotation in our modelView and normalView
  mat4 adjModelView = modelView;
  adjModelView[0][0] = 1.0;
  adjModelView[0][1] = 0.0;
  adjModelView[0][2] = 0.0;
  adjModelView[2][0] = 0.0;
  adjModelView[2][1] = 0.0;
  adjModelView[2][2] = 1.0;

  mat3 adjNormalView = normalView;
  adjNormalView[0][0] = 1.0;
  adjNormalView[0][1] = 0.0;
  adjNormalView[0][2] = 0.0;
  adjNormalView[2][0] = 0.0;
  adjNormalView[2][1] = 0.0;
  adjNormalView[2][2] = 1.0;
  
  // our on screen position by applying our model-view-projection matrix
  gl_Position = projection * adjModelView * V;
  
  // V after our model-view matrix is applied
  V = adjModelView * V;
  
  // N after our normalView matrix is applied
  Nv = normalize(adjNormalView * N);  
}
We're basically just doing our normal vertex shader logic here, just concentrating on the outputs actually used by our fragment shader, but with one weird little trick. We're resetting the X and Z vectors in our model view and normal view matrices. This will cancel out our entire rotation except for that on the Y axis. If we'd reset that to we'd always be looking straight at our square.

That was the easy part. Now for the hard part.

Render to texture


At this point we could go into blender, render our tree so it fills the entire screen and write it out to a texture and use that. For the most part that would be the way to go and I would recommend it. But I thought this would be a good opportunity to look at another neat trick and generate the texture inside of our engine. We are going to render to texture.

Rendering to texture is something that can be a really useful tool, it will be our main technique when we switch to deferred lighting but it is also often used to animate things in textures or for doing things like reflections. In those cases we're continuously rendering the texture each frame before rendering our real scene, in this example we're only using it one time to create our texture.

In our texture map library I've added two new functions:
  • tmapRenderToTexture initialises our texture as the destination to render too allocating or reusing things we need for this.
  • tmapFreeFrameBuffers frees up our frame buffers and depth buffer created when tmapRenderToTexture is called. It is automatically called when the texture map is being destructed but you can call it early if you don't want to hang on to the resources.
We'll have a look at tmapRenderToTexture in more detail:
// Prepare our texture so we can render to it. 
// if needed a new framebuffer will be created and made current.
// the calling routine will be responsible for unbinding the framebuffer.
bool tmapRenderToTexture(texturemap * pTMap, bool pNeedDepthBuffer) {
  if (pTMap == NULL) {
    return false;
  };

  // create our frame buffer if we haven't already
  if (pTMap->frameBufferId == 0) {
So the first time we call this for our texture we're going to set up what is called a frame buffer. This is a container in OpenGL that holds all the state that allows us to render to one or more textures.
    GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0 };
    GLenum status;

    glGenFramebuffers(1, &pTMap->frameBufferId);
    glBindFramebuffer(GL_FRAMEBUFFER, pTMap->frameBufferId);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pTMap->textureId, 0);
So here we've generated our frame buffer and then bound it by calling glBindFrameBuffer. Binding it makes it active and we can start using it. Once we're done we simply unbind it by setting the frame buffer to 0 and we're back to rendering to screen. We also call glFramebufferTexture2D to bind our texture to a color attachment. Color attachments will bind to outputs in our fragment shader so we can output colors to multiple textures. For now we just use GL_COLOR_ATTACHMENT0
    // bind our texture map
    // init and bind our depth buffer
    if ((pTMap->depthBufferId == 0) && pNeedDepthBuffer) {
      glGenTextures(1, &pTMap->depthBufferId);
      glActiveTexture(GL_TEXTURE0);
      glBindTexture(GL_TEXTURE_2D, pTMap->depthBufferId);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, pTMap->width, pTMap->height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
      glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, pTMap->depthBufferId, 0);
    };
This part is optional, here we generate and bind a depth buffer so we can do depth checks.
    // enable our draw buffers...
    glDrawBuffers(1, drawBuffers);
This last step tells us which of the textures we just bound will actually be used. Note that we've defined our array up top and are just activating our one texture.
    status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) {
      errorlog(status, "Couldn't init framebuffer (errno = %i)", status);
      tmapFreeFrameBuffers(pTMap);
      return false;
    };
Finally we check if all this was successful!
  } else {
    // reactivate our framebuffer
    glBindFramebuffer(FRAMEBUFFER, &pTMap->frameBufferId);
  };

  return true;
};
Last but not least, if we already created our frame buffer, we simply reselect it.

Now we're going to use our frame buffer just once to render an image of our tree and thus will free up our framebuffer once we're finished. That code can be found right after we've loaded our two tree meshes. Here is the code:
    // create our texture
    tmap = newTextureMap("treeLod3");
    tmapLoadData(tmap, NULL, 1024, 1024, GL_NEAREST, GL_CLAMP_TO_EDGE, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE);
    if (tmapRenderToTexture(tmap, true)) {
Here we've done 3 things, create our texture map object, initialize an empty 1024x1024 RGBA texture and called tmapRenderToTexture so we're ready to render our texture.
      shaderMatrices  matrices;
      lightSource     light;
      mat4            tmpmatrix;

      // set our viewport
      glViewport(0,0,1024,1024);

      // clear our texture
      glClearColor(0.0, 0.0, 0.0, 0.0);
      glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
This code should be pretty familiar as it is little different then rendering to screen, we set our viewport to cover our entire texture and clear our texture. Notice our we set our background to completely transparent!
      // setup our lightsource
      vec3Set(&light.position, 0.0, 1000000.0, 0.0);
      vec3Set(&light.adjPosition, 0.0, 1000000.0, 0.0);
      light.ambient = 1.0; // all ambient = no lighting
Here I'm cheating, I've set my ambient lighting factor to 1.0 so we basically have no lighting. This is where a lot of improvement can be made by setting up some proper lighting for rendering our texture.
      // setup matrices
      mat4Identity(&tmpmatrix);
      mat4Ortho(&tmpmatrix, -500.0, 500.0, 1000.0, 0.0, 1000.0f, -1000.0f);
      shdMatSetProjection(&matrices, &tmpmatrix);

      mat4Identity(&tmpmatrix);
      mat4LookAt(&view, vec3Set(&eye, 0.0, 500.0, 1000.0), vec3Set(&lookat, 0.0, 500.0, 0.0), vec3Set(&upvector, 0.0, 1.0, 0.0));  
      shdMatSetView(&matrices, &tmpmatrix);
I'm using an orthographic projection here, I thought that would give a nicer result. We also setup a very simple view matrix to ensure we're looking dead center at our tree mesh.
      // and render our tree to our texture
      meshNodeRender(treeLod2, &matrices, (material *) materials->first->data, &light);
And finally we render our tree.
      glBindFramebuffer(GL_FRAMEBUFFER, 0);
      tmapFreeFrameBuffers(tmap);
    };
And finally we make sure we unbind our frame buffer so our output goes to screen again and we free our frame buffer and voila, we have a texture!

Note that we're doing this all during our loading stage.

And here is our end result:



Download the source here

So where from here?


There is a lot of room for improvement but we've got all the basic ingredients now. For our render to texture we can experiment more with lighting and it would help to evaluate our shader a bit more as the flatness of the mesh makes it light up when you look towards the trees with the sun in your back.

Obviously using more detailed meshes, maybe adding a few more levels of details, and having different trees will result in a much nicer forest.

Finally rendering our leaves needs some shader enhancements to deal with the double sidedness of the leaves. On that same subject, adding some randomness here so each leaf is slightly differently colored would be a nice addition to the shader as well. Worth thinking about.

What's next?


Next time around we'll start looking at bounding boxes to skip rendering trees in our forest that aren't on screen so we can add more trees:)

No comments:

Post a Comment