changed JPanel to do my offscreen rendering

I’ve been struggling to get offscreen rendering to work with PBuffers for weeks under Windows, and finally I’ve given up. Really what I wanted was a way to render an image using OpenGL in a server environment where nothing was ever sent to the screen.

I finally got PBuffers working about a week ago, but it involved some ugly workarounds and they were very flaky. The program worked on some machines, but mostly I got the dang wglChoosePixelFormatARB exception. Also, the I never trusted the 0,0 window trick to provide an “onscreen” context for the PBuffer.

From reading other posts, I’m not the only one who has wanted to do something like this or has had problems, so I want to post my solution. It isn’t perfect or pretty, but it works on every windows machine I’ve tested.

Basically what I did was cut and pasted most of the JPanel code to make a component that just renders to an image. I don’t take any credit for this, because all I really did was take what I needed from the JOGL source:


import net.java.games.jogl.impl.*;
import net.java.games.jogl.*;
import java.awt.*;
import java.awt.image.*;

public class OffscreenPanel extends Component implements GLDrawable {
    
  private GLDrawableHelper drawableHelper = new GLDrawableHelper();
  private GLContext context;
  private BufferedImage offscreenImage;
  private int awtFormat;
  private int glFormat;
  private int glType;
  private int glComps;
  private DataBufferByte   dbByte;
  private DataBufferInt    dbInt;

  // For saving/restoring of OpenGL state during ReadPixels
  private int[] swapbytes    = new int[1];
  private int[] lsbfirst     = new int[1];
  private int[] rowlength    = new int[1];
  private int[] skiprows     = new int[1];
  private int[] skippixels   = new int[1];
  private int[] alignment    = new int[1];

  private final int fx      = 0;
  private final int fy      = 0;
  
  
  OffscreenPanel(GLCapabilities capabilities, GLCapabilitiesChooser chooser, int width, int height) {
    context = GLContextFactory.getFactory().createGLContext(null, capabilities, chooser, null);
    context.resizeOffscreenContext(width, height);   
    resizeContext(width, height);
  }

  public void resizeContext(int w, int h) {
      final int fwidth  = w;
      final int fheight = h;
      context.invokeGL(new Runnable() {
          public void run() {
              getGL().glViewport(fx, fy, fwidth, fheight);
              drawableHelper.reshape(OffscreenPanel.this, fx, fy, fwidth, fheight);
              if (offscreenImage != null &&
              (offscreenImage.getWidth()  != context.getOffscreenContextWidth() ||
              offscreenImage.getHeight() != context.getOffscreenContextHeight())) {
                  offscreenImage.flush();
                  offscreenImage = null;
              }
          }
      }, true, initAction);
  }
  
  public void display() {
     context.invokeGL(displayAction, false, initAction);
  }

  public BufferedImage getSnapshot () {
    return offscreenImage;
  }

  public void addGLEventListener(GLEventListener listener) {
    drawableHelper.addGLEventListener(listener);
  }

  public void removeGLEventListener(GLEventListener listener) {
    drawableHelper.removeGLEventListener(listener);
  }

  public GL getGL() {
    return context.getGL();
  }

  public void setGL(GL gl) {
    context.setGL(gl);
  }

  public GLU getGLU() {
    return context.getGLU();
  }

  public void setGLU(GLU glu) {
    context.setGLU(glu);
  }

  public void setRenderingThread(Thread currentThreadOrNull) throws GLException {
      throw new GLException("Not supported");
  }

  public Thread getRenderingThread() {
    return context.getRenderingThread();
  }

  public void setNoAutoRedrawMode(boolean noAutoRedraws) {
  }

  public boolean getNoAutoRedrawMode() {
    return false;
  }

  public boolean canCreateOffscreenDrawable() {
    return false;
  }

  public GLPbuffer createOffscreenDrawable(GLCapabilities capabilities,
                                           int initialWidth,
                                           int initialHeight) {
    throw new GLException("Not supported");
  }

  GLContext getContext() {
    return context;
  }

  //----------------------------------------------------------------------
  // Internals only below this point
  //

  class InitAction implements Runnable {
    public void run() {
      drawableHelper.init(OffscreenPanel.this);
    }
  }
  private InitAction initAction = new InitAction();

  class DisplayAction implements Runnable {

    public void run() {
      drawableHelper.display(OffscreenPanel.this);
      // Must now copy pixels from offscreen context into surface
      if (offscreenImage == null) {
        int awtFormat = context.getOffscreenContextBufferedImageType();
        offscreenImage = new BufferedImage(context.getOffscreenContextWidth(), context.getOffscreenContextHeight(), awtFormat);
        switch (awtFormat) {
          case BufferedImage.TYPE_3BYTE_BGR:
            glFormat = GL.GL_BGR;
            glType   = GL.GL_UNSIGNED_BYTE;
            glComps  = 3;
            dbByte   = (DataBufferByte) offscreenImage.getRaster().getDataBuffer();
            break;

          case BufferedImage.TYPE_INT_RGB:
            glFormat = GL.GL_BGRA;
            glType   = GL.GL_UNSIGNED_BYTE;
            glComps  = 4;
            dbInt    = (DataBufferInt) offscreenImage.getRaster().getDataBuffer();
            break;

          case BufferedImage.TYPE_INT_ARGB:
            glFormat = GL.GL_BGRA;
            glType   = context.getOffscreenContextPixelDataType();
            glComps  = 4;
            dbInt    = (DataBufferInt) offscreenImage.getRaster().getDataBuffer();
            break;

          default:
            throw new GLException("Unsupported offscreen image type " + awtFormat);
        }
      }

      GL gl = getGL();
      // Save current modes
      gl.glGetIntegerv(GL.GL_PACK_SWAP_BYTES,    swapbytes);
      gl.glGetIntegerv(GL.GL_PACK_LSB_FIRST,     lsbfirst);
      gl.glGetIntegerv(GL.GL_PACK_ROW_LENGTH,    rowlength);
      gl.glGetIntegerv(GL.GL_PACK_SKIP_ROWS,     skiprows);
      gl.glGetIntegerv(GL.GL_PACK_SKIP_PIXELS,   skippixels);
      gl.glGetIntegerv(GL.GL_PACK_ALIGNMENT,     alignment);

      gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES,    GL.GL_FALSE);
      gl.glPixelStorei(GL.GL_PACK_LSB_FIRST,     GL.GL_TRUE);
      gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH,    offscreenImage.getWidth());
      gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS,     0);
      gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS,   0);
      gl.glPixelStorei(GL.GL_PACK_ALIGNMENT,     1);

      // Actually read the pixels.
      gl.glReadBuffer(context.getOffscreenContextReadBuffer());
      if (dbByte != null) {
        gl.glReadPixels(0, 0, offscreenImage.getWidth(), offscreenImage.getHeight(), glFormat, glType, dbByte.getData());
      } else if (dbInt != null) {
        gl.glReadPixels(0, 0, offscreenImage.getWidth(), offscreenImage.getHeight(), glFormat, glType, dbInt.getData());
      }

      // Restore saved modes.
      gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES,  swapbytes[0]);
      gl.glPixelStorei(GL.GL_PACK_LSB_FIRST,   lsbfirst[0]);
      gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH,  rowlength[0]);
      gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS,   skiprows[0]);
      gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, skippixels[0]);
      gl.glPixelStorei(GL.GL_PACK_ALIGNMENT,   alignment[0]);

      gl.glFlush();
      gl.glFinish();

    }
  }
  private DisplayAction displayAction = new DisplayAction();
    
}

I have to reply to my own post, because it got to long. Continued below…

Continued…

To run the OffscreenPanel, I have something like this:


import net.java.games.jogl.*;
import net.java.games.jogl.util.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;

public class OffscreenGLEL implements GLEventListener  {
    
    public static void main(String [] args) {
        
        try {
            OffscreenGLEL test = new OffscreenGLEL();
            test.canvas.display();
            BufferedImage im = test.canvas.getSnapshot();
            ImageIO.write(im, "png", new File("image.png"));
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    OffscreenPanel canvas;
    int px = 256, py = 256;

    /** Creates a new instance of OffscreenGLEL */
    public OffscreenGLEL() {
        GLCapabilities cCaps = new GLCapabilities();
        cCaps.setHardwareAccelerated(false);
        cCaps.setDoubleBuffered(false);
        canvas = new OffscreenPanel(cCaps, null, px, py);
        canvas.addGLEventListener( this );        
        try {
            canvas.display();
        }
        catch (GLException e) {
            System.err.println(e.toString());
        }
    }
    
    public void displayChanged(GLDrawable drawable, boolean modeChanged, boolean deviceChanged)
    {
    // do the display changed stuff
    }

    public void reshape(GLDrawable drawable, int x, int y, int width, int height) 
    {
    // do the reshaping stuff
    }
    
     public void display(GLDrawable drawable) {
         // I have to call init() here to get that code to execute. This is bad
         // because I don't want to have to do the init stuff every time
         // I call display.
         
         init(drawable);
         
         // do all the drawing 
     }
     
     public void init(GLDrawable drawable) {
        // this call turns out to be critical, even though I'm not sure what it does
        reshape( drawable, 0, 0, px, py );

        // Add a light, etc.

    }
}

I also have a method in OffscreenGLEL that allows me to change the data controlling what is drawn in display(…). So I can create an OffscreenGLEL, set the data, run display(), capture the image using getSnapshot(), set new data, capture the new image, etc.

This actually works well for me and turns out to be much faster than doing something similar with hardware accelerated PBuffers. There must have been some overhead there. It also doesn’t require an onscreen context, and the texture does not have to be a power of 2.

My big problem right now is that I could not get init(…) to be called automatically. I now have to call it manually every time display() is called. This is inefficient, and I’d like to figure out a way to fix this.

Sorry for such a long post, but with all the other posts about offscreen rendering and the associated problems, I figured someone out there could benefit from this.

Daniel

Hi

I tried the classes and they work for me, except I cannot save a transparent PNG. All I get is an opaque PNG.


      public void display(GLDrawable drawable)
      {
            // I have to call init() here to get that code to execute. This is bad
            // because I don't want to have to do the init stuff every time
            // I call display.

            init(drawable);

            // do all the drawing  
            gl.glEnable(GL.GL_BLEND);  
            gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);              
      
            gl.glClearColor(0, 0, 0, 0);
            gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
            gl.glPushMatrix();
      
            
            gl.glEnable(GL.GL_TEXTURE_2D);

            gl.glBegin(GL.GL_QUADS);
            gl.glColor4f(1f, 1f, 1f, 0f);
            gl.glTexCoord2f(0, 0);
            gl.glVertex3i(-5, -5, -15);
            gl.glTexCoord2f(1, 0);
            gl.glVertex3i(5, -5, -15);
            gl.glTexCoord2f(1, 1);
            
            gl.glColor4f(1f, 0f, 0f,1f);
            gl.glVertex3i(5, 5, -15);
            gl.glTexCoord2f(0, 1);
            gl.glVertex3i(-5, 5, -15);
            gl.glEnd();
            gl.glDisable(GL.GL_TEXTURE_2D);
            gl.glPopMatrix();
      }

I’ve correctly set 8 alpha bits in the capabilities, but the alpha is still all opaque. Any hint ?
Should I render on a PBUFFER instead ?

Cheers,

Mik

This is great – could you please file an RFE / Patch on the JOGL Issues page containing this code?

Done.

Issue #93

Let me know…

Mik