LWJGL SpriteSheet class

Hi guys,

I just wanted to share my sprite sheet class that I have been working on in the hopes that it will help someone else. In essence, it loads an image, crops out each tile into a separate BufferedImage and finally creates a new texture from each sub image. The id of each “tile” is stored in a 1D array of texture and begins from the first tile and continues to the end of the sheet.

I designed this with RGBA images in mind to support transparency, but it should not be too hard to adapt this to other file formats.


package goldenkey.engine;

import static org.lwjgl.opengl.GL11.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.imageio.ImageIO;
import org.lwjgl.BufferUtils;

public class SpriteSheet
{
	private Texture[] textures;
	private BufferedImage sheet;
	private int width, height;
	private int tileSize;
	private int rows;
	private int cols;
	
	public SpriteSheet(String pathToFile, int tileSize)
	{
		FileInputStream fis;
		this.tileSize = tileSize;
		
		// Open the file and read in the spritesheet
		
		try 
		{
			fis = new FileInputStream(new File(pathToFile));
			sheet = ImageIO.read(fis);
			width = sheet.getWidth();
			height = sheet.getHeight();
			rows = height / tileSize;
			cols = width / tileSize;
			textures = new Texture[rows * cols];
			fis.close();
		}
		
		catch (IOException e) 
		{
			System.out.println(e.getMessage());
			System.exit(1);
		}

		
		// Crop out each sub image and create a texture from it
		
		crop();
		
	}
	
	private void crop()
	{
		for(int i=0; i<rows; i++)
		{
			for(int j=0; j<cols; j++)
			{
				BufferedImage temp = sheet.getSubimage(j*tileSize, i*tileSize, tileSize, tileSize);
				int id = generateTexture(temp);
				textures[i * cols + j] = new Texture(id,tileSize,tileSize);
			}
		}
	
	}
	
	private int generateTexture(BufferedImage image)
	{
		int[] pixels = image.getRGB(0, 0, tileSize, tileSize, null, 0, tileSize);
		ByteBuffer bb = BufferUtils.createByteBuffer((tileSize * tileSize) * 4);
		int id = glGenTextures();
		
		for(int i=0; i<pixels.length; i++)
		{
			byte r = (byte) ((pixels[i] >> 16) & 0xFF);
			byte g = (byte) ((pixels[i] >> 8) & 0xFF);
			byte b = (byte) ((pixels[i]) & 0xFF);
			byte a = (byte) ((pixels[i] >> 24) & 0xFF);
			
			bb.put(r);
			bb.put(g);
			bb.put(b);
			bb.put(a);
		}
		
		bb.flip();
		
		glBindTexture(GL_TEXTURE_2D, id);
		
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); 
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tileSize, tileSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, bb);
		
		glBindTexture(GL_TEXTURE_2D, 0);
		
		return id;
	}
	
	public Texture getTexture(int id)
	{
		if(id < 0 || id >= textures.length) return null;
		
		return textures[id];
	}
	
	public Texture[] getAll()
	{
		return textures;
	}
}

The Texture class is simply a wrapper class containing three fields: an int containing the texture id and a texture width and height.

I’m posting in the hopes that this will help someone else out there. :slight_smile:

I would suggest not chopping up the sprite sheet and uploading each chunk as a Texture. What I would do instead is simply upload the entire sheet as a texture and then draw only portions of it by using texture coordinates. I would also suggest using Matthias Mann’s open source pure-Java PNGDecoder (or one of his other decoders if you don’t want to use PNGs) to load your images. Both of these techniques can be read about in mattdesl’s Textures tutorial.

I appreciate your feedback, but I prefer this method for a few reasons. First, it eliminates the need to convert texture coordinates to normalized coordinates in the render loop. This is especially helpful when using VBO or vertex arrays for rendering because it prevents one from having to update the float buffers constantly.

While I realize that your suggestion is the “usual” approach, the chopping up of the sprite sheet occurs only once and therefore texture coordinates and vertex coordinates need only be initialized one time as well. However, I would love to see some benchmarking tests using both methods since I strive to keep my code as efficient as possible especially when it comes to rendering.

In regards to the PNGDecoder, I rather enjoyed the challenge and decided to write my own texture loading class which support PNG images. A good suggestion though for those that would rather not have to write such as a class themselves.

Thanks for reading my post and for your reply!

But it defeats the purpose of VBOs as you have lots of buffers for each texture.

Interesting. So it sounds like you are saying that the slowdown in the render loop might be worth it then? I am by no means an expert in OpenGL, so I really appreciate the feedback from more experienced programmers. I am using vertex arrays by the way because I didn’t like the fact that buffers needed to be deleted and the lack of a destructor in Java lol.

Calculate the texture coords of each subimage in the texture before hand.

Just make a helper class that stores objects that need to be disposed, and then have a method that disposes of everything.
Then you just call that method before the application exits.

Yeah; definitely don’t use a texture per sprite. The most important thing to do in OpenGL is batch, batch batch. As you progress you will find that you cannot achieve decent framerates (assuming your game is sprite-heavy) without a sprite batcher.

The basic idea of the sprite batcher is to fill as many sprites as we can into a single VBO (or vertex array). The batch will need to be “flushed” every time we switch textures. So if we have one texture per sprite, that means we are rendering only one sprite per flush. In other words, no batching is taking place, and we are back to the speeds of immediate mode.

This is especially important on mobile, where texture switching and batch flushing can be expensive.

An alternative GL3+ technique is to use a 2D texture array instead of a single texture atlas.

Hey guys,

I have rewritten my sprite sheet class based on everyone’s suggestions. It works but I am getting black vertical lines when moving my character but the lines disappear when the character stops. Any ideas?

Screenshot of issue:

https://dl.dropboxusercontent.com/u/39277637/screenshot.png

Here is the sprite sheet class:


package goldenkey.engine;

import static org.lwjgl.opengl.GL11.*;
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;

public class SpriteSheet
{
	private Texture texture;
	private FloatBuffer[] textureCoordinates;
	private FloatBuffer vertexData;
	private int tileSize;
	private int rows, cols;
	
	public SpriteSheet(Texture texture, int tileSize)
	{
		this.texture = texture;
		this.tileSize = tileSize;
		
		// Figure out how many texture coordinates we will have
		
		rows = texture.getHeight() / tileSize;
		cols = texture.getWidth() / tileSize;
	
		// Initialize the float buffers
		
		textureCoordinates = new FloatBuffer[rows * cols];
		vertexData = BufferUtils.createFloatBuffer(8);
		vertexData.put(new float[] {0,0,tileSize,0,tileSize,tileSize,0,tileSize});
		vertexData.flip();
		
		// Calculate all the texture coordinates ahead of time
		
		for(int i=0; i<rows; i++)
		{
			for(int j=0; j<cols; j++)
			{
				float srcX = j * tileSize;
				float srcY = i * tileSize;
				float u = srcX / texture.getWidth();
				float v = srcY / texture.getHeight();
				float u2 = (srcX + tileSize) / texture.getWidth();
				float v2 = (srcY + tileSize) / texture.getHeight();
				
				textureCoordinates[i * cols + j] = BufferUtils.createFloatBuffer(8);
				textureCoordinates[i * cols + j].put(new float[] {u,v,u2,v,u2,v2,u,v2});
				textureCoordinates[i * cols + j].flip();
			}
		}
		
	}
	
	public int getTileSize()
	{
		return tileSize;
	}
	
	public void draw(int tileID)
	{
		
		// Draw
		
		glEnableClientState(GL_VERTEX_ARRAY);
		glEnableClientState(GL_TEXTURE_COORD_ARRAY);
		
		glVertexPointer(2, 0, vertexData);
		glTexCoordPointer(2, 0, textureCoordinates[tileID]);
	
		texture.bind();
		glDrawArrays(GL_QUADS, 0, 4);
		
		glDisableClientState(GL_VERTEX_ARRAY);
		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
		
	}
}


I realize that this might not be the right place to post, but I wanted to try to revamp it based on your feedback.

That is texture bleeding. It happens when a texture is put partway between 2 pixels and texture data overflows to the next pixel.

There are a few solutions. Use Google to find the best for your situation.

Ok, half pixel correction seemed to cure the problem for me since I’m not using mipmapping. If anyone is interested this worked for me:

Changed this:


float u = srcX / texture.getWidth();
float v = srcY / texture.getHeight();

To this:


float u = (srcX + 0.5f) / texture.getWidth();
float v = (srcY + 0.5f) / texture.getHeight();

I guess this samples from the center of each texel. Whew!

Thanks everyone.