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 classRender
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 toSwapBuffers
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
istrue
. 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 toglBlendFunc
tells OpenGL exactly what type of blending we want to implement. Suffice to say that theGL_SRC_ALPHA
andGL_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 toGetCurrentFrame
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
andy
coordinates for the sprite based on thex
andy
values stored inm_position
. These values are used in theglVertex2f
calls to position the sprite. - We will also need the
width
andheight
of the current frame, and the next two lines store these asw
andh
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
andglVertex2f
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 piding 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:
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 pidingm_textureIndex
bym_numberOfFrames
- We determine the starting u value by multiplying
m_currentFrame
bytexWidth
- 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 index0
- 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)