Question

I'm coding a Java LWJGL game, and everything's going along great, except whenever I try to figure out a way to create a BufferedImage of the current game area. I've searched the internet, browsed all of the opengl functions, and I am getting no where... Anyone have any ideas? Here's all I have so far, but it only makes a blank .png:

if(Input.getKeyDown(Input.KEY_F2)) {
    try {
        String fileName = "screenshot-" + Util.getSystemTime(false);
        File imageToSave = new File(MainComponent.screenshotsFolder, fileName + ".png");
        int duplicate = 0;
        while(true) {
            duplicate++;
            if(imageToSave.exists() == false) {
                imageToSave.createNewFile();
                break;
            }
            imageToSave = new File(MainComponent.screenshotsFolder, fileName + "_" + duplicate + ".png");
        }
        imageToSave.createNewFile();

        // Create a buffered image:
        BufferedImage image = new BufferedImage(MainComponent.WIDTH, MainComponent.HEIGHT, BufferedImage.TYPE_INT_ARGB);
        //Wrtie the new buffered image to file:
        ImageIO.write(image, "png", imageToSave);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
Was it helpful?

Solution

You never actually write something into your BufferedImage.

Read the Buffer

You can use glReadPixels to access the selected buffer. (I assume WIDTH and HEIGHT as your OpenGLContext dimensions.)

FloatBuffer imageData = BufferUtils.createFloatBuffer(WIDTH * HEIGHT * 3); 
GL11.glReadPixels(0, 0, WIDTH, HEIGHT, GL11.GL_RGB, GL11.GL_FLOAT, imageData);
imageData.rewind();

Use whatever parameters suit your needs best, I just picked floats randomly.

Set the Image Data

You already figured out how to create and save your image, but in between you should also set some content to the image. You can do this with BufferedImage().setRGB() (Note that I don't use a good naming as you do, to keep this example concise.)

// create image
BufferedImage image = new BufferedImage(
     WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB
);

// set content
image.setRGB(0, 0, WIDTH, HEIGHT, rgbArray, 0, WIDTH);

// save it
File outputfile = new File("Screenshot.png");
try {
    ImageIO.write(image, "png", outputfile);
} catch (IOException e) {
    e.printStackTrace();
}

The most tricky part is now getting the rgbArray. The problems are that

  1. OpenGL gives you three values (in this case, i.e. using GL11.GL_RGB), while the BufferedImage expects one value.
  2. OpenGL counts the rows from bottom to top while BufferedImage counts from top to bottom.

Calculate one Integer from three Floats

To get rid of problem one you have to calculate the integer value which fits the three number you get.
I will show this with a simple example, the color red which is (1.0f, 0.0f, 0.0f) in your FloatBuffer.

For the integer value it might be easy to think of numbers in hex values, as you might know from CSS where it's very common to name colors with those. Red would be #ff0000 in CSS or in Java of course 0xff0000.

Colors in RGB with integers are usually represented from 0 to 255 (or 00 to ff in hex), while you use 0 to 1 with floats or doubles. So first you have to map them to the correct range by simply multiplying the values by 255 and casting them to integers:

int r = (int)(fR * 255);

Now you can think of the hex value as just putting those numbers next to each other:

rgb = 255 0 0 = ff 00 00

To achieve this you can bitshift the integer values. Since one hex value (0-f) is 4 byte long, you have to shift the value of green 8 bytes to the left (two hex values) and the value of red 16 bytes. After that you can simply add them up.

int rgb = (r << 16) + (g << 8) + b;

Getting from BottomUp to TopDown

I know the terminology bottom-up -> top-down is not correct here, but it was catchy.

To access 2D data in a 1D array you usually use some formula (this case row-major order) like

int index = offset + (y - yOffset) * stride + (x - xOffset);

Since you want to have the complete image the offsets can be left out and the formula simplified to

int index = y * stride + x;

Of course the stride is simply the WIDTH, i.e. the maximum achievable x value (or in other terms the row length).

The problem you now face is that OpenGL uses the bottom row as row 0 while the BufferedImage uses the top row as row 0. To get rid of that problem just invert y:

int index = ((HEIGHT - 1) - y) * WIDTH + x;

Filling the int[]-array with the Buffer's Data

Now you know how to calculate the rgb value, the correct index and you have all data you need. Let's fill the int[]-array with those information.

int[] rgbArray = new int[WIDTH * HEIGHT];
for(int y = 0; y < HEIGHT; ++y) {
    for(int x = 0; x < WIDTH; ++x) {
        int r = (int)(imageData.get() * 255) << 16;
        int g = (int)(imageData.get() * 255) << 8;
        int b = (int)(imageData.get() * 255);
        int i = ((HEIGHT - 1) - y) * WIDTH + x;
        rgbArray[i] = r + g + b;
    }
}

Note three things about this little piece of code.

  1. The size of the array. Obviously it's just WIDTH * HEIGHT and not WIDTH * HEIGHT * 3 as the buffer's size was.
  2. Since OpenGL uses row-major order, you have to use the column value (x) as the inner loop for this 2D array (and of course there are other ways to write this, but this seemed to be the most intuitive one).
  3. Accessing imageData with imageData.get() is probably not the safest way to do it, but since the calculations are carefully done it should do the job just fine. Just remember to flip() or rewind() the buffer before calling get() the first time!

Putting it all together

So with all the information available now we can just put a method saveScreenshot() together.

private void saveScreenshot() {
    // read current buffer
    FloatBuffer imageData = BufferUtils.createFloatBuffer(WIDTH * HEIGHT * 3); 
    GL11.glReadPixels(
        0, 0, WIDTH, HEIGHT, GL11.GL_RGB, GL11.GL_FLOAT, imageData
    );
    imageData.rewind();

    // fill rgbArray for BufferedImage
    int[] rgbArray = new int[WIDTH * HEIGHT];
    for(int y = 0; y < HEIGHT; ++y) {
        for(int x = 0; x < WIDTH; ++x) {
            int r = (int)(imageData.get() * 255) << 16;
            int g = (int)(imageData.get() * 255) << 8;
            int b = (int)(imageData.get() * 255);
            int i = ((HEIGHT - 1) - y) * WIDTH + x;
            rgbArray[i] = r + g + b;
        }
    }

    // create and save image
    BufferedImage image = new BufferedImage(
         WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB
    );
    image.setRGB(0, 0, WIDTH, HEIGHT, rgbArray, 0, WIDTH);
    File outputfile = getNextScreenFile();
    try {
        ImageIO.write(image, "png", outputfile);
    } catch (IOException e) {
        e.printStackTrace();
        System.err.println("Can not save screenshot!");
    }
}

private File getNextScreenFile() {
    // create image name
    String fileName = "screenshot_" + getSystemTime(false);
    File imageToSave = new File(fileName + ".png");

    // check for duplicates
    int duplicate = 0;
    while(imageToSave.exists()) {
        imageToSave = new File(fileName + "_" + ++duplicate + ".png");
    }

    return imageToSave;
}

// format the time
public static String getSystemTime(boolean getTimeOnly) {
    SimpleDateFormat dateFormat = new SimpleDateFormat(
      getTimeOnly?"HH-mm-ss":"yyyy-MM-dd'T'HH-mm-ss"
    );
    return dateFormat.format(new Date());
}

I also uploaded a very simple full working example.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top