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.
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.
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); }
Here's how it works:
glClear
sets the entire color buffer to the background color that we chose when initializing OpenGL. glLoadIdentify
resets the rendering matrix.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.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.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:
m_visible
is true
. If not, we bypass the entire render.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.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.width
and height
of the current frame, and the next two lines store these as w
and h
for convenience.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.glEnd
tells OpenGL that we are finished with the current render.glDisable(GL_BLEND)
.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:
texWidth
) by dividing m_textureIndex
by m_numberOfFrames
m_currentFrame
by texWidth
u
+ texWidth
u
+ texWidth
to the lower corner of the quadWe 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:
m_textures[0]
because, by definition, there is only one texture at index 0
m_currentFrame
. m_currentFrame
is updated in the sprite update method (defined next)18.117.233.26