LWJGL Forum

Programming => OpenGL => Topic started by: CodeBunny on August 10, 2011, 21:14:28

Title: Saving Screenshots
Post by: CodeBunny on August 10, 2011, 21:14:28
I realized I hadn't yet figured out how to save screen shots, and so I'm trying to work it out, but am having issues that I don't understand.

I want to be able to create save a screen shot in a variable format (probably via ImageIO.write()). So, I decided to try to use glReadPixels to create a ByteBuffer or IntBuffer of the pixel data. Then, I would create and save an Image with the data in another thread, so as to lighten the burden on the main game loop.

Here's what I have so far:

// Start the process
public static void takeScreenShot(File file, String format, int x, int y, int width, int height)
{
new ImageSavingThread(file, format, captureScreen(x, y, width, height), width, height).run();
}

// Capture the screen data in an IntBuffer.
public static IntBuffer captureScreen(int x, int y, int width, int height)
{
IntBuffer screenBuffer = ByteBuffer.allocateDirect(width * height * 16).asIntBuffer(); // Why x16? Shouldn't it be x4? It crashes when I try, however.
GL11.glReadPixels(x, height - y, width, height, GL11.GL_RGB, GL11.GL_INT, screenBuffer); // To allow the supplied coordinates to be in standard 2D.
return screenBuffer;
}

// The internal Thread class that handles actually saving the captured data.
public static class ImageSavingThread extends Thread
{
protected File file;
protected String format;
protected IntBuffer buffer;
protected int width;
protected int height;

public ImageSavingThread(File file, String format, IntBuffer buffer,
int width, int height)
{
this.file = file;
this.format = format;
this.buffer = buffer;
this.width = width;
this.height = height;
}

public void run()
{
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
for(int i = 0; i < width; i ++)
for(int j = 0; j < height; j++)
image.setRGB(i, j, ((j * width) + i) * 64); // I'm just randomly guessing here, I admit it.
buffer.clear();
try {
ImageIO.write(image, format, file);
} catch (IOException e) { e.printStackTrace(); }
}
}


Here's the resulting image (what the game was and what I got):

(http://img37.imageshack.us/img37/8035/screenshotcorrect.jpg)

(http://img850.imageshack.us/img850/8370/screenshothr.jpg)


Well, that sort of worked (it created an image of the correct dimensions in the target directory, and the game didn't crash), but is totally wrong.

I'll admit I don't really know what I'm doing here. I've had some experience with BufferedImages, but not with ByteBuffers and/or IntBuffers, and especially not with setting the raster to represent the supplied pixel data. So: what am I doing wrong?

Additionally, what's the fastest possible way to create a screen shot with LWJGL? Right now this takes about 400 ms to read the pixels and start the other Thread (haven't measured Thread operation time itself), but is it possible to do this more quickly? If so, what's the process?
Title: Re: Saving Screenshots
Post by: broumbroum on August 10, 2011, 21:32:39
You can instance a java.util.Timer that will run a TimerTask each time you want to take such a screenshot.Thus avoiding Thread creation you may have a sensible gain of time...
Title: Re: Saving Screenshots
Post by: CodeBunny on August 10, 2011, 22:35:25
But what if the Display is changed before the glReadPixels finishes? Wouldn't that cause some bad results?
Title: Re: Saving Screenshots
Post by: jediTofu on August 11, 2011, 04:46:28
Since you're reading as RGB, it would need to be width*height*3.

Possibly glReadBuffer is not set to correct buffer?

Do gl calls work in threads outside the main one (without using special gl stuff)?  I thought LWJGL works best all in same thread?

Here's my guess based on setRGB:

if(buffer.hasArray)
  image.setRGB(0,0,width,height,buffer.array(),0,0);

Or:

int[] array = buffer.array();
for(int y = 0; y < height; ++y)
  for(int x = 0; x < width; ++x)
    image.setRGB(x,y,array[y * width + x]);


cool story bro  8)
Title: Re: Saving Screenshots
Post by: CodeBunny on August 11, 2011, 12:55:05
The point of spinning off the separate thread is that I do it only once I've captured the screen data. The new Thread handles saving the image, because that takes a potentially large amount of time and all interfacing with OpenGL is done.

Of course, if this is a stupid/unnecessary/performance degrading measure please tell me, but it seems to me that the overhead of saving the image in the game loop is much lower than creating a new, short-lived thread.




When I try the new way of putting pixel data into the BufferedImage, I get an all-black result. Maybe I'm reading the buffer wrong?

Here's my update method to capture the screen data:


public static IntBuffer captureScreen(int x, int y, int width, int height)
{
GL11.glReadBuffer(GL11.GL_FRONT);
ByteBuffer screenBuffer = ByteBuffer.allocateDirect(width * height * 3 * 4);
GL11.glReadPixels(x, height - y, width, height, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, screenBuffer);
return screenBuffer.asIntBuffer();
}


A couple of questions:

I would expect that I would need to size the buffer to width * height * 3. After all, there are width * height pixels, and I have 3 color channels, each at a byte resolution. Then why do I need to have x4 that number of bytes? It seems like the method expects 32 bits for each color channel, instead of 8.

What does glReadPixels do in LWJGL? There's no javadoc on this method, and no tutorials, so I'm kind of lost.
Title: Re: Saving Screenshots
Post by: broumbroum on August 11, 2011, 21:23:31
Quote from: CodeBunny on August 11, 2011, 12:55:05
...
Here's my update method to capture the screen data:


...
ByteBuffer screenBuffer = ByteBuffer.allocateDirect(width * height * 3 * 4);
}

...
You need to reorder bytes : ByteBuffer screenBuffer = ByteBuffer.allocateDirect(width * height * 3 * 4).order(ByteOrder.nativeOrder());
Title: Re: Saving Screenshots
Post by: CodeBunny on August 12, 2011, 03:20:30
No change.

*sigh* I guess I need an example. Does anyone have a working example of capturing the Display contents into a BufferedImage? Actual working code would be of immense help.
Title: Re: Saving Screenshots
Post by: jediTofu on August 12, 2011, 04:25:23
If you remember that old icon thread we had, this is my method from that thread, which I tested then and worked.
It would just be a matter of putting it into the Image.  This probably isn't the most efficient since I'm using bytes.


 public ByteBuffer createIcon(int width,int height,boolean fixAlphas,boolean makeBlackTransparent,Texture texture) {
   //int drawBuffer = glGetInteger(GL_DRAW_BUFFER);

   //Draw & stretch a width by height icon onto the back buffer
   //glDrawBuffer(GL_BACK);
   texture.bind();
   glBegin(GL_QUADS);
     glTexCoord2f(0,0); glVertex2f(0    ,0);
     glTexCoord2f(1,0); glVertex2f(width,0);
     glTexCoord2f(1,1); glVertex2f(width,height);
     glTexCoord2f(0,1); glVertex2f(0    ,height);
   glEnd();

   //Read the back buffer into the byte buffer icon
   //glReadBuffer(GL_BACK);
   ByteBuffer icon = BufferUtils.createByteBuffer(width * height * 4);
   glReadPixels(0,0,width,height,GL_RGBA,GL_BYTE,icon);

   //fixAlphas:            In case of OpenGL blending and/or bitmap problems
   //makeBlackTransparent: Cycle through and set black to be transparent
   if(fixAlphas || makeBlackTransparent) {
     for(int y = 0; y < height; y++) {
       for(int x = 0; x < width; x++) {
         int color = y * 4 * width + x * 4;
         int red   = icon.get(color);
         int green = icon.get(color + 1);
         int blue  = icon.get(color + 2);

         if(makeBlackTransparent && red == 0 && green == 0 && blue == 0) {
           icon.put(color + 3,(byte)0);
         }
         else if(fixAlphas) {
           icon.put(color + 3,(byte)255);
         }
       }
     }
   }

   //Set back to original draw buffer
   //glDrawBuffer(drawBuffer);

   return(icon);
 }
Title: Re: Saving Screenshots
Post by: jediTofu on August 12, 2011, 04:28:45
Also, if you're wanting the most speed, you should use the same type (RGB, RGBA, etc.) in glReadPixels as the display.  If the display is in RGBA and glReadPixels is having to convert it to RGB, it will be slower due to conversion.
Title: Re: Saving Screenshots
Post by: CodeBunny on August 12, 2011, 16:05:52
Sweet, thanks for the advice. I've got it sort of working now. :D

Only the resulting screenshot is really dark.

Here's what I should get:

(http://img88.imageshack.us/img88/9136/screenshotcorrect.png)

But here's what the result is:

(http://img190.imageshack.us/img190/5143/screenshotlv.png)

Why is it so dark?

Here's how I read the display:


public static ByteBuffer captureScreen(int x, int y, int width, int height)
{
GL11.glReadBuffer(GL11.GL_FRONT);
ByteBuffer screenBuffer = BufferUtils.createByteBuffer(width * height
* 4);
    GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_BYTE,
    screenBuffer);
return screenBuffer;
}


Here's how I put the bytes into a BufferedImage:


public void run() // All of this code happens in a separate thread.
{
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
for(int i = 0; i < width; i++)
for(int j = 0; j < height; j++)
{
int bufferPlace = (i + (width * j)) * 4;
byte red = buffer.get(bufferPlace);
byte green = buffer.get(bufferPlace + 1);
byte blue = buffer.get(bufferPlace + 2);
int color = (0xFF << 24) | // For a completely opaque image.
(red << 16) |
(green << 8) |
(blue);
image.setRGB(i, height - (j + 1), color);
}
try {
ImageIO.write(image, format, file);
} catch (IOException e) { e.printStackTrace(); }
}


Am I doing anything wrong? I thought that maybe using GL_UNSIGNED_BYTE would work but that has the opposite problem and is much too bright.



NOTE: Because I'm handling saving the image in a separate thread, the screenshot time taken in the main game loop is down to <100 milliseconds even on large resolutions. :D
Title: Re: Saving Screenshots
Post by: Matthias on August 12, 2011, 16:30:07
Take a look here (http://www.matthiasmann.de/content/view/23/26/) - it's a small tutorial on how to take screenshots from OpenGL and save them as TGA or PNG file in a background thread so that it won't block your OpenGL thread.
Title: Re: Saving Screenshots
Post by: CodeBunny on August 12, 2011, 17:30:46
Very nice, but the only problem is that it's format limited. I actually want to use the ImageIO methods with a ByteBuffer/BufferedImage, because it allows a built-in flexibility with the format.
Title: Re: Saving Screenshots
Post by: Matthias on August 12, 2011, 19:00:06
You mean build in slowness and memory overhead?
Title: Re: Saving Screenshots
Post by: CodeBunny on August 12, 2011, 19:53:20
Probably, yeah. But still, the format flexibility is what I'm aiming for.

I do really like the code you offered. It just doesn't quite suit what I'm attempting to do.
Title: Re: Saving Screenshots
Post by: jediTofu on August 13, 2011, 10:40:14
Maybe your bufferPlace is off?

Also, you definitely shouldn't be using i in setRGB at least.

I'd do some print/println of your bufferPlace, i, and the RGB values into a text file.
Title: Re: Saving Screenshots
Post by: CodeBunny on August 13, 2011, 12:22:13
The buffer place isn't off, the pixels are in the correct locations. The color values are just 1/2 what they should be.

For example, if I replace this:


int color = (0xFF << 24) | // For a completely opaque image.
(red << 16) |
(green << 8) |
(blue);
image.setRGB(i, height - (j + 1), color);


With this:


image.setRGB(i, height - (j + 1), color << ((0xFF << 24) |
(red << 16) |
(green << 8) |
(blue)) << 1);


It works. Basically, by bit-shifting, the color values work.

To me this looks like a sign issue. Unfortunately, when I try to use glReadPixels with a GL_UNSIGNED_INT, I get the opposite problem - it's too bright.

I would use this code since because it works fine; however, I want to be sure that this bug isn't machine-dependent. This feels a little "hackish."
Title: Re: Saving Screenshots
Post by: jediTofu on August 13, 2011, 23:14:54
Try this:


int color = (0xFF << 24) | // For a completely opaque image.
((red & 0xFF) << 16) |
((green & 0xFF) << 8) |
(blue & 0xFF);
image.setRGB(i, height - (j + 1), color);


If this doesn't work, I'd try using longs (but type would still be GL_UNSIGNED_INT).

EDIT:  also try "& 0xFF" after the shifts instead.
Title: Re: Saving Screenshots
Post by: CodeBunny on August 14, 2011, 11:42:20
Doesn't work.

What do you mean I should do with longs?
Title: Re: Saving Screenshots
Post by: jediTofu on August 15, 2011, 05:29:51
This code worked for me:


        // read
        int bpp = 4;
        int height = HEIGHT;
        int width = WIDTH;
        ByteBuffer screenBuffer = BufferUtils.createByteBuffer(width * height * bpp);
        glReadPixels(0,0,width,height,GL_RGBA,GL_UNSIGNED_BYTE,screenBuffer);

        // write
        BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
        for(int y = 0; y < height; ++y) {
          for(int x = 0; x < width; ++x) {
            int i = y * width * bpp + x * bpp;
            int r = screenBuffer.get(i) & 0xFF;
            int g = screenBuffer.get(i + 1) & 0xFF;
            int b = screenBuffer.get(i + 2) & 0xFF;
            image.setRGB(x,y,(0xFF << 24) | (r << 16) | (g << 8) | b);
          }
        }

        // save
        try {
          ImageIO.write(image,"PNG",new File("test.png"));
        }
        catch(Throwable t) {
          t.printStackTrace();
        }
Title: Re: Saving Screenshots
Post by: CodeBunny on August 15, 2011, 12:06:45
Sweet, that worked! Thank you very much! :D

I feel like this should be put up in the wiki as a tutorial. Does anyone else want to do it, or should I? I'm perfectly willing to, I just don't have a wiki account.
Title: Re: Saving Screenshots
Post by: jediTofu on August 16, 2011, 02:50:47
I can when I get some free time from work/life :)  but I'm sure they are perfectly fine with giving you a wiki account.  maybe message Kappa?  I think he edits the wiki the most
Title: Re: Saving Screenshots
Post by: CodeBunny on August 16, 2011, 12:35:29
Okay, I will.
Title: Re: Saving Screenshots
Post by: CodeBunny on August 16, 2011, 23:14:56
I put it up on the wiki:

http://lwjgl.org/wiki/index.php?title=Taking_Screen_Shots (http://lwjgl.org/wiki/index.php?title=Taking_Screen_Shots)
Title: Re: Saving Screenshots
Post by: kappa on August 17, 2011, 08:44:59
oh nice work, pretty well written tutorial.
Title: Re: Saving Screenshots
Post by: CodeBunny on August 17, 2011, 12:13:12
Haha I saw the tweet. Thanks!