Tutorial 02 - Textures and Sprites

In this tutorial, we'll look at ways you can load textures and create sprites from them to display. Again, we'll start with the full source listing, then go into details. However, I'm leaving out everything that's been covered, so please read tutorial 01 first or refer to it for anything you can't find here.

It's Textures and Sprites time, then:

#include <XRhodes/xrhodes.hpp> const Uint UP_DIR = 2; const Float UPDATE_DELAY = 1.0f / 60; const int SCR_W = 640; const int SCR_H = 480; bool ProcessEvents(SDL_Event &e) { while(::SDL_PollEvent(&e) == 1) { if(e.type == SDL_QUIT) { return false; } } return true; } int main(int argc, char *argv[]) { XR::Core core(argv[0], UP_DIR, UPDATE_DELAY, &std::cout); XR::GFX::Init(SCR_W, SCR_H); GLuint hTexSprite(XR::Texture2D::Generate()); String texName(XR::pCore->GetPath() + "02_texture.png"); ::SDL_Surface *pSurf(::IMG_Load(texName.c_str())); if(pSurf == 0) { XR::pCore->GetBuffer() << XR::MSG_ERR << "SDL couldn't load texture from " << texName << std::endl; exit(2); } ::SDL_Color colorKey = { BYTE_MAX, 0, BYTE_MAX }; if(!XR::Texture2D::Create(pSurf, &colorKey)) { XR::pCore->GetBuffer() << XR::MSG_ERR << "Couldn't import SDL_Surface." << std::endl; exit(2); } ::SDL_FreeSurface(pSurf); XR::Sprite::Creator sc; sc.hTexture = hTexSprite; sc.texCoords = XR::WHOLE_TEXTURE; XR::Sprite sprite(sc); XR::Texture2D::Creator tc; tc.size = XR::GFX::GetViewport().size; tc.bytesPerPixel = 4; tc.pixels = 0; sc.hTexture = XR::Texture2D::Generate(tc); XR::Sprite feedback(sc); SDL_Event e; Float tDelta; Float tUpdate(.0f); Float tRender(.0f); while(ProcessEvents(e)) { tDelta = XR::Clock::Query(); tUpdate -= tDelta; if(tUpdate <= .0f) { XR::QueryKeys(); if(XR::GetKeyState(SDLK_ESCAPE).isPressed) { e.type = SDL_QUIT; ::SDL_PushEvent(&e); } tUpdate = XR::pCore->GetUpdateDelay(); } tRender -= tDelta; if(tRender <= .0f) { XR::GFX::Clear(); XR::Auto::Enable __e(GL_BLEND); XR::Auto::PushMatrix __p; ::glTranslatef(SCR_W / 2, SCR_H / 2, .0f); { Float scale(3.0f); XR::Auto::PushMatrix __p; ::glRotatef(20, .0f, .0f, 1.0f); ::glScalef(scale, -scale, 1.0f); XR::CYAN.Apply(.96f); feedback.Draw(); } { XR::Auto::Enable __e(GL_TEXTURE_2D); XR::Texture2D::Bind(hTexSprite); XR::WHITE.Apply(); sprite.Draw(); } XR::Texture2D::Capture(feedback.GetTextureHandle()); XR::GFX::Present(); tRender = XR::pCore->GetRenderDelay(); } } XR::Texture2D::Delete(hTexSprite); XR::Texture2D::Delete(feedback.GetTextureHandle()); return 0; }

Texture handling is based on how OpenGL handles textures and has been greatly simplified in XRhodes, however it might still seem a little peculiar at first.

GLuint hTexSprite(XR::Texture2D::Generate());

A texture is identified by a GLuint value (handle; hence the 'h' prefix), which you can obtain by having XRhodes generate you one. Don't forget to initialize the graphics before attempting any of the OpenGL operations. XRhodes' Texture2D::Generate() will bind you the newly generated texture, thus preparing it for data being imported into it. (We'll come back to binding the texture later on.)

String texName(XR::pCore->GetPath() + "02_texture.png");
When you instantiate the core, XR::Core, a pointer to the instance will be saved to XR::pCore. You can query this for the path of the executable, the update / render rate and the primary text output buffer you have specified. Note: you might have to change the UP_DIR value depending on where your project library is relative to your executable (with xcode, on Mac, you want it to be 5).
::SDL_Surface *pSurf(::IMG_Load(texName.c_str())); if(pSurf == 0) //... (error handling) ... ::SDL_Color colorKey = { BYTE_MAX, 0, BYTE_MAX }; if(!XR::Texture2D::Create(pSurf, &colorKey)) //... (error handling) ...

The next step is the actual import, for which you have several options. Here we'll load a texture into an SDL_Surface pointer, and pass that to Texture2D::Create() (of course, you must make sure that there is a valid pointer first). You could also just pass the absolute path of your texture file to Texture2D::Create() to load it directly. Here's an image you can use:

fail

Passing it the SDL_Surface pointer allows us to specify a colorkey for transparency (optional), which is what we did here - we want all deathpink pixels of our bitmap to be transparent in the resulting texture. All Texture2D::Create() methods will return a boolean value to indicate the success of the operation. You mustn't continue on failure. For this demo, printing an error message and aborting is good enough.

::SDL_FreeSurface(pSurf);

As we've brought the SDL_Surface pointer pSurf to this world, it's our responsibility to deallocate the memory it uses. If we're creating a texture directly from a file, XRhodes takes care of the deallocation for us.

XR::Sprite::Creator sc; sc.hTexture = hTexSprite; sc.texCoords = XR::WHOLE_TEXTURE; XR::Sprite sprite(sc);

Next we're creating a Sprite based on the texture. In OpenGL, the texture is an abstract idea, you can't directly display it - you need something to display it on, or map it to. Sprite is a rectangular (quadrilateral) polygon with four coordinate pairs of our 2D texture mapped to it.

The Sprite is created from a Sprite::Creator which comprises of a valid texture handle and a set of coordinates presented in the form of an XR::AABB (Axis Aligned Bounding Box). The XR::AABB struct contains four members, left, top, right, and bottom. XR::WHOLE_TEXTURE is a constant XR::AABB that will map the whole texture (.0, .0, 1.0, 1.0) to the sprite.

(With XRhodes, you can create a sprite directly from an image file, by specifying its absolute path. This, however is only recommended for prototyping, as each of these sprites will use an individual texture handle (as you'll see later, you can save some processing time by keeping texture binding to a minimum), plus you can only use the whole texture this way.)

XR::Texture2D::Creator tc; tc.size = XR::GFX::GetViewport().size; tc.bytesPerPixel = 4; tc.pixels = 0;

We're creating one more texture here, which we'll use for a neat visual effect: texture feedback. We need a texture that's the size of the screen (viewport).

XRhodes automatically sets the viewport to be the size of the full screen initially. You can set it to your desired size and position with XR::GFX::SetViewport() and setting up your projection (which is also done automatically in the beginning), however that's beyond the scope of this tutorial; please refer to the documentation.

sc.hTexture = XR::Texture2D::Generate(tc); XR::Sprite feedback(sc);

What you see above is the shorthand texture generation: it generates a new handle, then forwards the data that you've passed to a compatible Texture2D::Create() method. If there was an error, this method returns XR::INVALID_GL_ID (in this case the handle will be properly cleaned up). Note that you can't use colorkeying this way (at least as of v0.83).

If you've got a video card that doesn't support NPOT textures (whose either dimension is Non-Power Of Two, hence the acronym), your application might crash at this point with an OpenGL error code 1281. If this happens, try modifying the values of SCR_W and SCR_H to 512.

As you'll see, we're using the same Sprite::Creator object. Once a Sprite is created from it, it's no longer needed it and we can reuse it. Make sure, however that you keep track of your texture handles, as you will need to delete them when cleaning up (we have hTexSprite for that purpose).

Also note how we didn't change the texture coordinates since the last Sprite. This is because OpenGL uses a percentage value between .0 and 1.0 to refer to texels. .0 will always refer to the left / top, 1.0 will always refer to the right / bottom, regardless of how many pixels apart are they.

The game loop skeleton is the same as before. The fun part is what we've changed between XR::GFX::Clear() and XR::GFX::Present():

XR::Auto::Enable __e(GL_BLEND);

We'll enable GL_BLEND for the time being, as we want to use transparency.

XR::Auto::PushMatrix __p; ::glTranslatef(SCR_W / 2, SCR_H / 2, .0f);

We'll save the transformation matrix then move to the middle of the screen. This must be familiar from the last tutorial.

Float scale(3.0f); XR::Auto::PushMatrix __p; ::glRotatef(20, .0f, .0f, 1.0f); ::glScalef(scale, -scale, 1.0f);

We'll save the transformation matrix, as we only want to rotate and scale the feedback. Now, here's an important bit: OpenGL will capture the texture upside down. Therefore, you will have to apply a negative vertical scaling to get it back upright.

XR::CYAN.Apply(.96f); feedback.Draw();

We then change the color and Draw() the feedback. This is the shorthand draw function that will take care of some of the tasks that need to be done working with OpenGL, namely enabling 2D texturing and binding the texture. We'll see all this in details next:

XR::Auto::Enable __e(GL_TEXTURE_2D); XR::Texture2D::Bind(hTexSprite); XR::WHITE.Apply(); sprite.DrawOnly();

OpenGL is a state machine, and as it was said before, state changes take up processing time, because we might have to wait for the OpenGL pipeline to finish what it's doing first. Enabling / disabling OpenGL capabilities and binding a texture all incur state changes. If you are using a texture as a sprite sheet, if you know that you will be using texturing for an extended period, you can do the enabling for all of your operations, then you can group texture bindings, and use DrawOnly() which will only render the textured quads for you.

Notice how we changed the color to XR::WHITE so as to display the sprite without tinting.

XR::Texture2D::Capture(feedback.GetTextureHandle());

Then we will write to the texture that's used for feedback what we've drawn to the screen. XR::Texture2D offers a variety of Capture() functions. What's provided in v0.83 will enable the texturing and will bind the given texture for you, however, as of v0.84 you'll be also offered a leaner version, which will capture to a texture already bound.

XR::Texture2D::Delete(hTexSprite); XR::Texture2D::Delete(feedback.GetTextureHandle());

Finally, we clean up after ourselves.

Exercises:

  1. Change the texture coordinates used in sc. Observe the effect.
  2. Change the color applied to the sprite. Observe the effect.
  3. Change the order of sprite and feedback being drawn. Observe the effect.
  4. Apply animation to the sprite: move it, rotate it. Observe the effect.

Back to Resources.