Java-Gaming.org    
Featured games (79)
games approved by the League of Dukes
Games in Showcase (475)
Games in Android Showcase (106)
games submitted by our members
Games in WIP (530)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  LWJGL SpriteSheet class  (Read 1377 times)
0 Members and 1 Guest are viewing this topic.
Offline songangel

Junior Member


Medals: 1



« Posted 2013-08-08 06:24:05 »

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.

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  
91  
92  
93  
94  
95  
96  
97  
98  
99  
100  
101  
102  
103  
104  
105  
106  
107  
108  
109  
110  
111  
112  
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.   Smiley
Offline Z-Man
« Reply #1 - Posted 2013-08-09 20:50:25 »

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.
Offline songangel

Junior Member


Medals: 1



« Reply #2 - Posted 2013-08-11 00:51:33 »

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!
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline HeroesGraveDev

JGO Kernel


Medals: 238
Projects: 11
Exp: 2 years


┬─┬ノ(ಠ_ಠノ)(╯°□°)╯︵ ┻━┻


« Reply #3 - Posted 2013-08-11 01:02:38 »

This is especially helpful when using VBO or vertex arrays for rendering because it prevents one from having to update the float buffers constantly.

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

Offline songangel

Junior Member


Medals: 1



« Reply #4 - Posted 2013-08-11 01:12:47 »

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.
Offline StumpyStrust
« Reply #5 - Posted 2013-08-11 01:14:17 »

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

Offline HeroesGraveDev

JGO Kernel


Medals: 238
Projects: 11
Exp: 2 years


┬─┬ノ(ಠ_ಠノ)(╯°□°)╯︵ ┻━┻


« Reply #6 - Posted 2013-08-11 01:38:50 »

I didn't like the fact that buffers needed to be deleted and the lack of a destructor in Java lol.

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.

Offline davedes
« Reply #7 - Posted 2013-08-11 02:08:17 »

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.

https://github.com/mattdesl/lwjgl-basics/wiki/Sprite-Batching

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.

Offline songangel

Junior Member


Medals: 1



« Reply #8 - Posted 2013-08-11 03:03:56 »

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:



Here is the sprite sheet class:
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
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.
Offline HeroesGraveDev

JGO Kernel


Medals: 238
Projects: 11
Exp: 2 years


┬─┬ノ(ಠ_ಠノ)(╯°□°)╯︵ ┻━┻


« Reply #9 - Posted 2013-08-11 03:25:10 »

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.

Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline songangel

Junior Member


Medals: 1



« Reply #10 - Posted 2013-08-11 03:50:55 »

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:
1  
2  
float u = srcX / texture.getWidth();
float v = srcY / texture.getHeight();


To this:
1  
2  
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.
Pages: [1]
  ignore  |  Print  
 
 
You cannot reply to this message, because it is very, very old.

 

Add your game by posting it in the WIP section,
or publish it in Showcase.

The first screenshot will be displayed as a thumbnail.

ctomni231 (34 views)
2014-07-18 06:55:21

Zero Volt (30 views)
2014-07-17 23:47:54

danieldean (25 views)
2014-07-17 23:41:23

MustardPeter (27 views)
2014-07-16 23:30:00

Cero (42 views)
2014-07-16 00:42:17

Riven (44 views)
2014-07-14 18:02:53

OpenGLShaders (32 views)
2014-07-14 16:23:47

Riven (32 views)
2014-07-14 11:51:35

quew8 (30 views)
2014-07-13 13:57:52

SHC (66 views)
2014-07-12 17:50:04
HotSpot Options
by dleskov
2014-07-08 03:59:08

Java and Game Development Tutorials
by SwordsMiner
2014-06-14 00:58:24

Java and Game Development Tutorials
by SwordsMiner
2014-06-14 00:47:22

How do I start Java Game Development?
by ra4king
2014-05-17 11:13:37

HotSpot Options
by Roquen
2014-05-15 09:59:54

HotSpot Options
by Roquen
2014-05-06 15:03:10

Escape Analysis
by Roquen
2014-04-29 22:16:43

Experimental Toys
by Roquen
2014-04-28 13:24:22
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!