Saving Screenshots

Started by CodeBunny, August 10, 2011, 21:14:28

Previous topic - Next topic

CodeBunny

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):






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?

broumbroum

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

CodeBunny

But what if the Display is changed before the glReadPixels finishes? Wouldn't that cause some bad results?

jediTofu

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)
cool story, bro

CodeBunny

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.

broumbroum

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());

CodeBunny

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.

jediTofu

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);
  }
cool story, bro

jediTofu

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.
cool story, bro

CodeBunny

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:



But here's what the result is:



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

Matthias

Take a look here - 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.

CodeBunny

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.

Matthias

You mean build in slowness and memory overhead?

CodeBunny

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.

jediTofu

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.
cool story, bro