如何从OpenGL窗口获取BufferedImage?

时间:2014-02-22 02:22:12

标签: java opengl bufferedimage

我正在编写一个Java LWJGL游戏,一切都很顺利,除非我试图找到一种方法来创建当前游戏区域的BufferedImage。我搜索了互联网,浏览了所有的opengl函数,我没有在哪里......任何人都有任何想法?这是迄今为止我所拥有的一切,但它只是一个空白.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();
    }
}

1 个答案:

答案 0 :(得分:4)

你永远不会在你的BufferedImage中写点东西。

阅读缓冲区

您可以使用glReadPixels访问所选缓冲区。 (我假设WIDTH和HEIGHT为您的OpenGLContext维度。)

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

使用最适合您需求的参数,我只是随机选择浮动。

设置图像数据

您已经了解了如何创建和保存图像,但在两者之间,您还应该为图像设置一些内容。您可以使用BufferedImage().setRGB()执行此操作(请注意,我没有像您一样使用良好的命名,以保持此示例简洁。)

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

最棘手的部分是现在获得rgbArray。问题在于

  1. OpenGL为您提供三个值(在这种情况下,即使用GL11.GL_RGB),而BufferedImage则需要一个值。
  2. OpenGL从下到上计算行数,而BufferedImage从上到下计数。
  3. 从三个Floats计算一个整数

    要摆脱问题,你必须计算出适合你得到的三个数字的整数值 我将通过一个简单的例子来展示这一点,红色是FloatBuffer中的(1.0f, 0.0f, 0.0f)

    对于整数值,可能很容易想到十六进制值中的数字,正如您可能从CSS中知道的那样,用这些数字命名颜色是很常见的。红色在CSS或Java中#ff0000当然是0xff0000

    RGB中带有整数的颜色通常表示为0到255(或十六进制中的00到ff),而使用浮点数或双精度时使用0到1。首先,您必须将值乘以255并将它们转换为整数,将它们映射到正确的范围:

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

    现在您可以将十六进制值视为将这些数字放在一起:

    rgb = 255 0 0 = ff 00 00
    

    要实现此目的,您可以对整数值进行位移。由于一个十六进制值(0-f)长度为4个字节,因此必须将绿色8字节的值向左移动(两个十六进制值),并将红色16字节的值移位。之后,您可以简单地添加它们。

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

    从BottomUp到TopDown

    我知道自下而上的术语 - &gt;自上而下在这里不正确,但它很吸引人。

    要访问一维数组中的二维数据,您通常会使用一些公式(例如行主要顺序),如

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

    由于您想拥有完整的图像,因此可以省略偏移,并将公式简化为

    int index = y * stride + x;
    

    当然,stride只是WIDTH,即最大可实现x值(或其他方面的行长度)。

    你现在面临的问题是OpenGL使用底行作为行0,而BufferedImage使用顶行作为行0.要摆脱这个问题,只需反转y:

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

    使用Buffer的数据

    填充int [] - 数组

    现在您知道如何计算rgb值,正确的索引并获得所需的所有数据。让我们用这些信息填充int [] - 数组。

    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;
        }
    }
    

    注意关于这段小代码的三件事。

    1. 数组的大小。显然它只是WIDTH * HEIGHT而不是WIDTH * HEIGHT * 3,因为缓冲区的大小是。
    2. 由于OpenGL使用行主要顺序,你必须使用列值(x)作为这个2D数组的内部循环(当然还有其他方法可以写这个,但这似乎是最直观的一个) )。
    3. 使用imageData.get()访问imageData可能不是最安全的方法,但由于仔细完成计算,它应该可以正常工作。在第一次致电flip()之前,请记住rewind()get()缓冲区!
    4. 全部放在一起

      因此,现在可以获得所有可用信息,我们可以将方法saveScreenshot()放在一起。

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

      我还上传了一个非常简单的full working example