Day 12: Image Sprites – Part 1

Introduction

Up to now we have successfully implemented a texture manager to properly load and unload textures when needed. We also implemented the general behavior of a sprite, regardless of its type. Today we are going to implement a common form of sprite, which is called “Image Sprite” in our framework. An image sprite consists of a series of images taken from an atlas, which it uses as frames. Apart from the normal behavior of a sprite, an image sprite can be animated through changing its frames.

Frames and cutouts

Before we begin our image sprite implementation, we need to create a way to take out its frames from a given atlas. For simplicity, we assume that all the frames corresponding to a sprite always stick to each other in the form of a grid, and they are not scattered on the atlas. With this assumption, we can implement a class for this specific purpose: Cutout.

Texture Atlas

How does it work?

We learned that in order to draw a texture, we need to define texture coordinates (the portion of the texture to draw). Previously, we always drew the entire texture. When working with texture atlases, this is no longer the case. This is where our Cutout class comes to play. It simply calculates texture coordinates corresponding to each frame, and holds those coordinates in a list. Whenever a frame is to be drawn, its coordinates is looked up in this list, and passed to OpenGL ES.

The basic structure

We now begin implementing. As this class is only used in image sprite, we create the ImageSprite class and add Cutout as a static inner class. The first thing we need to add is the actual list of texture coordinates.

package annahid.libs.artenus.graphics.sprites;

import java.nio.FloatBuffer;

public class ImageSprite extends Sprite {
	public static final class Cutout {
		private FloatBuffer[] textureBuffers;
	}
}

The only challenge now is creating this array.

Cutout parameters

Next we should devise a way to instruct the cutout. We make a further assumption here, that all frames have the same width and height. Note that this does NOT mean that an atlas can only hold frames of the same size, but that frames corresponding to each sprite on an atlas must be of the same size. So we need the following fields to characterize the cutout.

private float frameWidth, frameHeight;
private int cols, rows;
private int startX, startY;

The first line defines the size of a frame. The second line is the size of the grid. Also, this grid is not always located at the top-left corder of the image atlas. That’s why we added another degree of freedom, and added a starting point. When building the texture coordinates, they are offset by startX horizontally and startY vertically.

Constructors

As we perform lazy loading on textures, we must do the same for cutouts. That’s because texture sizes might not be available when constructing cutouts. So we must delay our texture coordinate generation to when they are definitely available, which is when they are to be first drawn. This simplifies our constructor to the level of just filling the fields:

public Cutout(float frameWidth, float frameHeight, int cols, int rows, int startX, int startY) {
	this.frameWidth = frameWidth;
	this.frameHeight = frameHeight;
	this.cols = cols;
	this.rows = rows;
	this.startX = startX;
	this.startY = startY;
}

We also add other convenience constructors, for special cases where the atlas only contains one grid, or when the grid only has one row:

public Cutout(float frameWidth, float frameHeight, int frameCountW, int frameCountH) {
	this(frameWidth, frameHeight, frameCountW, frameCountH, 0, 0);
}

public Cutout(float frameWidth, float frameHeight, int frameCount) {
	this(frameWidth, frameHeight, frameCount, 1, 0, 0);
}

Generating the coordinates

We now define the function that generates the coordinates. This function will later be called by the image sprite when necessary. Knowing the number of rows and columns in the grid, we know how many coordinates we need to generate:

void generate(int w, int h) {
	textureBuffers = new FloatBuffer[cols * rows];

	for(int row = 0; row < rows; row++)
		for(int col = 0; col < cols; col++) {
			// TODO: Generate texture coordinate for the current item
		}
}

The parameters w and h determine the width and height of the atlas. Give the row and the column, we first calculate the end-points by converting the original atlas image coordinates to OpenGL texture coordinates. If you remember from day 7, texture mappings come in the range of 0 to 1. For example for an image of width 100, texture coordinate 0 corresponds to the point 0 and texture coordinate 1 corresponds to the point 99 (the last point). We take this into consideration when building the buffers:

final float x1 = (startX + frameWidth * (float)col) / (float)w;
final float x2 = (startX + frameWidth * (float)(col + 1)) / (float)w;
final float y1 = (startY + frameHeight * (float)row) / (float)h;
final float y2 = (startY + frameHeight * (float)(row + 1)) / (float)h;

The rest is similar to what we did in day 7, and is necessary to create the native buffer:

final float texture[] = {
	x1, y1,
	x2, y1,
	x1, y2,
	x2, y2,
};

final ByteBuffer ibb = ByteBuffer.allocateDirect(texture.length * 4);
ibb.order(ByteOrder.nativeOrder());

final FloatBuffer textureBuffer = ibb.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
textureBuffers[row * cols + col] = textureBuffer;

External properties

The Cutout class is now complete. However, it needs some getters in order to be fully usable. The most important getter is one to determine whether or not the generate function has been called. We don't want the image sprite to call this function each time it draws the sprite, because once created, texture coordinates can live through different app states. So, it is enough to call them only once. This getter is simple to implement:

boolean isGenerated() { return textureBuffers != null; }

Note that once generate is called, textureBuffers cannot be null.

We also define two other properties for our Cutout class, which don't seem necessary for now, but might be needed for some use cases:

public float getFrameWidth() {
	return frameWidth;
}

public float getFrameHeight() {
	return frameHeight;
}

You may place getters for other fields too if you feel the need.

Next steps

For every part 1, there is a part 2! Next day we will complete the image sprite class, and see how Cutout is used in it. Once image sprite implementation is complete, we can start creating animations using our framework. But building real games needs interactivity, which will be discussed later on in this guide. If you find the code in this guide patchy and have a hard time assembling it, please feel free to download the ImageSprite class as per today's progress: