Rendering

We did a lot of work creating our sprites, but nothing is going to show up until we actually render the sprites using OpenGL. Rendering is done for every frame of the game. First, an Update function is called to update the state of the game, then everything is rendered to the screen.

Adding a render to the game loop

Let's start by adding a call to Render in the GameLoop RoboRacer.cpp:

void GameLoop()
{
  Render();
}

At this point, we are simply calling the main Render function (implemented in the next section). Every object that can be drawn to the screen will also have a Render method. In this way, the call to render the game will cascade down through every renderable object in the game.

Implementing the main Render function

Now, it is time to implement the main Render function. Add the following code to RoboRacer.cpp:

void Render()
{
  glClear(GL_COLOR_BUFFER_BIT);
  glLoadIdentity();
  
  background->Render();
  robot_left->Render();
  robot_right->Render();
  robot_left_strip->Render();
  robot_right_strip->Render();
  
  SwapBuffers(hDC);
}

Tip

Notice that we render the background first. In a 2D game, the objects will be rendered in a first come, first rendered basis. This way the robot will always render on top of the background.

Here's how it works:

  • We always start our render cycle by resetting the OpenGL render pipeline. glClear sets the entire color buffer to the background color that we chose when initializing OpenGL. glLoadIdentify resets the rendering matrix.
  • Next, we call Render for each sprite. We don't care if the sprite is actually visible or not. We let the sprite class Render method make that decision.
  • Once all objects are rendered, we make the call to SwapBuffers. This is a technique known as double-buffering. When we render our scene, it is actually created in a buffer off screen. This way the player doesn't actually see the separate images as they are composited to the screen. Then, a single call to SwapBuffers makes a fast copy of the offscreen buffer to the actual screen buffer. This makes the screen render appear much more smoothly.

Implementing Render in the Sprite class

The last step in our render chain is to add a render method to the Sprite class. This will allow each sprite to render itself to the screen. Open Sprite.h and add the following code:

void Sprite::Render()
{
  if (m_isVisible)
  {
    if (m_useTransparency)
    {
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }
    
    glBindTexture(GL_TEXTURE_2D, GetCurrentFrame());
    
    glBegin(GL_QUADS);
    
    GLfloat x = m_position.x;
    GLfloat y = m_position.y;
    
    GLfloat w = m_size.width;
    GLfloat h = m_size.height;
    
    GLfloat texWidth = (GLfloat)m_textureIndex / (GLfloat)m_numberOfFrames;
    GLfloat texHeight = 1.0f;
    GLfloat u = 0.0f;
    GLfloat v = 0.0f;
    if (m_textureIndex < m_numberOfFrames)
    {
      u = (GLfloat)m_currentFrame * texWidth;
    }
    glTexCoord2f(u, v); glVertex2f(x, y);
    glTexCoord2f(u + texWidth, v); glVertex2f(x + w, y);
    glTexCoord2f(u + texWidth, v + texHeight); glVertex2f(x + w, y + h);
    glTexCoord2f(u, v + texHeight); glVertex2f(x, y + h);
    
    glEnd();
    
    if (m_useTransparency)
    {
      glDisable(GL_BLEND);
    }
  }
}

This is probably one of the more complex sections of the code because rendering has to take many things into consideration. Is the sprite visible? Which frame of the sprite are we rendering? Where on screen should the sprite be rendered? Do we care about transparency? Let's walk through the code step by step:

  • First, we check to see if m_visible is true. If not, we bypass the entire render.
  • Next, we check to see if this sprite uses transparency. If it does, we have to enable transparency. The technical term to implement transparency is blending. OpenGL has to blend the current texture with what is already on the screen. glEnable(GL_BLEND) turns on transparency blending. The call to glBlendFunc tells OpenGL exactly what type of blending we want to implement. Suffice to say that the GL_SRC_ALPHA and GL_ONE_MIUS_SRC_ALPHA parameters tell OpenGL to allow background images to be seen through transparent sections of the sprite.
  • glBindTexture tells OpenGL which texture we want to work with right now. The call to GetCurrentFrame returns the OpenGL handle of the appropriate texture.
  • glBegin tells OpenGL that we are ready to render a particular item. In this case, we are rendering a quad.
  • The next two lines of code set up the x and y coordinates for the sprite based on the x and y values stored in m_position. These values are used in the glVertex2f calls to position the sprite.
  • We will also need the width and height of the current frame, and the next two lines store these as w and h for convenience.
  • Finally, we need to know how much of the texture we are going to render. Typically, we render the entire texture. However, in the case of a sprite sheet we will only want to render a section of the texture. We will discuss how this works in more detail later.
  • Once we have the position, width, and portion of the texture that we want to render, we use for pairs of calls to glTexCoord2f and glVertex2f to map each corner of the texture to the quad. This was discussed in great detail in Chapter 2, Your Point of View.
  • The call to glEnd tells OpenGL that we are finished with the current render.
  • As alpha checking is computationally expensive, we turn it off at the end of the render with a call to glDisable(GL_BLEND).

UV mapping

UV mapping was covered in detail in Chapter 2, Your Point of View. However, we'll do a recap here and see how it is implemented in code.

By convention, we assign the left coordinate of the texture to the variable u, and the top coordinate of the texture to the variable v. This technique is therefore known as uv mapping.

OpenGL considers the origin of a texture to be at uv coordinates of (0, 0), and the farthest extent of the texture to be at uv coordinates of (1, 1). So, if we want to render the entire texture, we will map the entire range from (0, 0) to (1, 1) the four corners of the quad. However, let's say that we only want to render the first half of the image width (but the entire height). In this case, we will map the range of uv coordinates from (0, 1) to (0.5, 1) to the four corners of the quad. Hopefully, you can visualize that this will only render one-half of the texture.

So, in order to render our sprite sheets, we first determine how wide each frame of the sprite is by dividing m_textureIndex by m_numberOfFrames. In the case of a sprite that has four frames, this will give us a value of 0.25.

Next, we determine which frame we are in. The following table shows the uv ranges for each frame of a sprite with four frames:

Frame

u

v

0

0.0 to 0.25

0.0 to 1.0

1

0.25 to 0.5

0.0 to 1.0

2

0.5 to 0.75

0.0 to 1.0

3

0.75 to 1.0

0.0 to 1.0

As our sprite sheets are set up horizontally, we only need to worry about taking the correct range of u from the whole texture, while the range for v stays the same.

So, here is how our algorithm works:

  • If the sprite is not a sprite sheet, then each frame uses 100 percent of the texture, and we use a range of uv values from (0,0) to (1, 1)
  • If the sprite is based on a sprite sheet, we determine the width of each frame (texWidth) by dividing m_textureIndex by m_numberOfFrames
  • We determine the starting u value by multiplying m_currentFrame by texWidth
  • We determine the extent of u by adding u + texWidth
  • We map u to the upper-corner of the quad, and u + texWidth to the lower corner of the quad
  • v is mapped normally because our sprite sheets use 100 percent of the height of the texture

Tip

If you are having a hard time understanding uv mapping, don't fret. It took me years of application to fully understand this concept. You can play around with the uv coordinates to see how things work. For example, try settings of .05, 1, and 1.5 and see what happens!

One more detail

We need to take a closer look at the call to GetCurrentFrame to make sure you understand what this function does. Here is the implementation:

const GLuint GetCurrentFrame()
{
  
  if(m_isSpriteSheet)
  {
    return m_textures[0];
  }
  else
  {
    return m_textures[m_currentFrame];
  }
}

Here is what is happening:

  • If the sprite is a sprite sheet, we always return m_textures[0] because, by definition, there is only one texture at index 0
  • If the sprite is not a sprite sheet, then we return the texture at index m_currentFrame. m_currentFrame is updated in the sprite update method (defined next)
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.233.26