JOGL and OpenGL Differences

There are key differences between using JOGL and OpenGL, due partly from the fact that JOGL maps a function-based C API to object-oriented Java, and partly from the fact that Java is secure, (here we mean in terms of illegal memory access) and OpenGL is not. These differences instantly make the JOGL interface slightly different everywhere, and very different in a few critical places. In addition, not every single API difference is documented here (depending on how you look at it, there are hundreds!), and inevitably there will be new differences in future JOGL versions. This book is meant to help you understand the major differences and learn to use them in Java.

OpenGL’s Static Constants and Functions

The C version of OpenGL makes heavy use of static constants throughout the OpenGL API, particularly for function parameters. It is an accepted way to create some level of type-safety for the C language interface it uses. However, in Java there is no such thing as true, stand-alone (classless) static variables or constants. All Java constructs are either a field or method of a class. Java objects may have static class variables but not any global classless static variables such as those used in C and OpenGL.

OpenGL also uses static functions. This is the nature of pure C code. Similar to the static constants, Java has no true classless methods that can be mapped directly to the OpenGL C functions.

This is a typical problem when mapping a functional C API to a object-oriented language such as Java. Fortunately, the solution is simple and effective in most cases. A single Java class is created that has all the C APIs static variables and functions, which are mapped to identically or similarly named static (or instance) variables and methods. The developer then either accesses the variables/functions through the classes’ static variables/functions directly, or, depending on the design, a runtime instance is created and used as the accessing object.

JOGL uses this second method of a runtime class instantiation for various reasons. That is, a special GL object is created, and most JOGL operations are done using that single instance. To an experienced OpenGL programmer this process may appear strange at first, and to anyone using existing OpenGL resources, any sample C-based OpenGL code will not port unmodified but must be converted to this class instance-accessing method.

Fortunately, for a Java programmer the conversion is quite simple and straightforward. An example code segment follows, first in C, then in Java with JOGL:

// Original C OpenGL source glBegin( GL_QUADS );     glColor3f( 1.0, 0.0, 0.0 );     glVertex3f( 10.0, 0.0, 0.0 );     glColor3f( 0.0, 1.0, 0.0 );     glVertex3f( 0.0, 10.0, 0.0 );     glColor3f( 0.0, 0.0,1.0 );     glVertex3f( -10.0, 0.0, 0.0 );     glColor3f( 1.0, 1.0, 1.0 );     glVertex3f( 0.0, -10.0, 0.0 ); glEnd(); // Ported to JOGL // using local gl instance reference // and GL class reference // and explicit f for floats gl.glBegin( GL.GL_QUADS );     gl.glColor3f( 1.0f, 0.0f, 0.0f );     gl.glVertex3f( 10.0f, 0.0f, 0.0f );     gl.glColor3f( 0.0f, 1.0f, 0.0f );     gl.glVertex3f( 0.0f, 10.0f, 0.0f );     gl.glColor3f( 0.0f, 0.0f,1.0f );     gl.glVertex3f( -10.0f, 0.0f, 0.0f );     gl.glColor3f( 1.0f, 1.0f, 1.0f );     gl.glVertex3f( 0.0f, -10.0f, 0.0f ); gl.glEnd();

OpenGL’s Use of C Pointers to Arrays

Several functions in OpenGL make use of pointers to arrays in C. This is done as a mechanism to return a series of OpenGL names, or handles, for what it calls server-side data objects. For example, multiple texture names can be generated at once in the C API by calling:

void glGenTextures(GLsizei n,                    GLuint *textures)

where n specifies the number of texture names to be generated and *textures specifies a pointer to an array in which the generated texture names are stored. The GLuint *textures is a C pointer to GLuint that should be at the beginning of an array of GLuint type that is the length of n. Because arrays are a formal type in Java, JOGL simply uses Java array objects instead.

public void glGenTextures(int n,                           int[] textures)

After calling this method, the Java int[] array argument “textures” will contain the texture names OpenGL has generated (just like it would for Gluint *texture in C) and can be accessed in the usual Java way.

This method shows another difference as well. In C, OpenGL has all sorts of additional primitive data types usage beyond standard C types to help type-safe OpenGL.

JOGL has mapped the OpenGL types to native Java types in the JNI layer where applicable, so standard Java types are supported directly. See Table 13.1.

Table 13.1 :  OPENGL TYPES MAPPED TO NATIVE JAVA TYPES

OpenGL Data Type

Internal Representation

Defined as C Type

C Literal Suffix

Java Type

Glbyte

8-bit integer

Signed char

b

byte

Glubyte

8-bit unsigned integer

Unsigned char

ub

byte

Glboolean

8-bit unsigned integer

Unsigned char

boolean

 

Glshort

16-bit integer

Short

s

short

GLushort

16-bit unsigned integer

Unsigned short

us

short

GLint, Glsizei

32-bit integer

Long

I

int

GLuint, GLenum, GLbitfield

32-bit unsigned integer

Unsigned long

ui

int

GLfloat, Glclampf

32-bit floating point

Float

f

float

GLdouble, GLclampd

64-bit floating point

Double

d

double

The C literal suffix is used in the function names in OpenGL, and the same suffixes are used in JOGL for compatibility and listed here for reference. The way the functions are named generally follows as shown in Figure 13.1.

image from book
Figure 13.1: The first two letters are the library prefix, which is a common design in C APIs. gl means that the function is part of the gl library, vertex is the command, 3 gives the number of arguments for this function, f denotes that the arguments are of type float.

Creating JOGL Textures

To reveal a more profound JOGL difference, we will further examine setting up textures in JOGL.

Java has existing standard images classes, including image loaders, and it would be nice to be able to use those to create the textures for OpenGL. It’s not automatic, but it’s not terribly difficult, either. It also exposes another significant difference in JOGL—its use of ByteBuffer objects.

ByteBuffers

JOGL needs fast and efficient ways to process and reference chucks of “raw” memory that would contain texture data or geometry data to which the OpenGL layer can have direct access, just as it does in C. The Java solution is to use NIO’s ByteBuffers wherever JOGL needs that kind of direct access. So, many functions that expect C pointers in OpenGL alternatively access ByteBuffers in the Java bindings and under-the-covers on the JNI-side OpenGL, which can have direct access to this data without any wasteful copying.

Let’s look at how this affects the texture functions by following the process of creating a texture from a standard Java BufferImage.

Getting a texture into an OpenGL environment is at least a three-step process on any system. Because of all the existing standard Java image APIs, it’s probably one of the easiest environments.

Step 1—Load a image from the file system: Image loading is a well-supported functionality in Java, so this is probably the most familiar step for the typical Java developer. Several supported mechanisms are available for loading images, and this can always be done though direct file access as well, if needed.

Step 2—Format the image data appropriately: The JOGL texture methods accept texture data as ByteBuffers where the buffer data must be encoded to a format that the OpenGL standard accepts. Unfortunately, this is almost never going to be the same format as the data format returned from a loaded image, unless the image loader was specifically designed for JOGL, which none of the existing standard image loaders are. Therefore, some data conversion will be required to get from the loaded image data to the OpenGL required data format.

Step 3—Create OpenGL texture binding for the new texture and set the texture data and parameters: After the ByteBuffer is packed with the image data correctly formatted, we can get a texture bind ID from OpenGL and pass the texture ByteBuffer along with any OpenGL texture parameters that we want to set with it.

Of the methods to load images in Java that the example uses, is ImageIO.read(), which returns BufferedImages. BufferedImages are the preferred type because they support a pathway to get the image data to a byte[] array with which we can load up a ByteBuffer for OpenGL. One problem that we wouldn’t catch until actually viewing in OpenGL is that the image would be upside down in most cases. This happens in many systems; it is not a Java-specific issue.

What happens is that the convention for assigning texture coordinates is with positive U (X in the image) to the right and positive V (Y in the image) up. But most image formats consider the positive y-direction as down, as it is in screen space. It is simple to flip the image with the AffineTransformOp class right after loading but before the OpenGL formatting, if needed. Given the correct string filename for an image file, here’s how it can be done:

static public  BufferedImage loadImage(String resourceName)  throws IOException {     BufferedImage bufferedImage = null;     try     {         bufferedImage = ImageIO.read(new File(resourceName));     }     catch (Exception e)     {         // file not found or bad format maybe      // do something good     }     if (bufferedImage != null)     {         // Flip Image         //         AffineTransform tx = AffineTransform.getScaleInstance(1, -        1);         tx.translate(0, -bufferedImage.getHeight(null));         AffineTransformOp op = new AffineTransformOp(tx,         AffineTransformOp.TYPE_NEAREST_NEIGHBOR);         bufferedImage = op.filter(bufferedImage, null);     }     return bufferedImage; }

At this point we have a BufferedImage in memory. Now it must be formatted for OpenGL.

This process could be a bit messy in practice, because BufferedImages can themselves be many different data formats. The trick is to use the Java image APIs and let them handle the conversion for you. Using ComponentColorModel and the Raster class, we can set up a utility routine that can convert any BufferedImage to an OpenGL-acceptable data format. First, we will make two reusable ComponentColorModels, one for regular RGB color images and another for RGBA (color-transparent) images. Then we will use the ComponentColorModels to build a Raster from which we can get bytes.

One last detail is that textures need to be sized to powers of 2 due to most graphics hardware requirements. It is simple to check the image’s width and height, and upsize the image height and/or width to the next power of 2. This action is typically considered wasteful in terms of graphics memory because the image will be larger but not anymore detailed. However, it will allow loading of non-power-of-2-size images without failing out, which is especially useful when testing.

ByteBuffer AllocateDirect and Native Order

There are two additional important points about using ByteBuffers in OpenGL. The ByteBuffers need to be created with ByteBuffer.allocateDirect() so that the ByteBuffers are C accessible without copying, and the byte order must be set to native order with byteBuffer.order(ByteOrder.nativeOrder()) to work correctly. Failing to do this is a common error when first starting out with JOGL.

Putting this all together, we have a versatile image conversion routine.

glAlphaColorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),     new int[]{8, 8, 8, 8}, true, false, ComponentColorModel.TRANSLUCENT, DataBuffer.TYPE_BYTE); glColorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),     new int[]{8, 8, 8, 0}, false, false, ComponentColorModel.OPAQUE, DataBuffer.TYPE_BYTE); public static ByteBuffer convertImageData(BufferedImage bufferedImage) throws TextureFormatException {     ByteBuffer imageBuffer = null;     try     {         WritableRaster raster;         BufferedImage texImage;         int texWidth = 2;         int texHeight = 2;         while (texWidth #### bufferedImage.getWidth())         {             texWidth *= 2;         }         while (texHeight #### bufferedImage.getHeight())         {             texHeight *= 2;         }         if (bufferedImage.getColorModel().hasAlpha())         {             raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, texWidth,  texHeight, 4, null);             texImage = new BufferedImage(glAlphaColorModel, raster, false, new Hashtable());         }         else         {             raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, texWidth,  texHeight, 3, null);             texImage = new BufferedImage(glColorModel, ####dis:raster, false, new Hashtable());         }         texImage.getGraphics().drawImage(bufferedImage, 0, 0, null);         byte[] data = ((DataBufferByte) texImage.getRaster().getDataBuffer()).getData();         imageBuffer = ByteBuffer.allocateDirect(data.length);         imageBuffer.order(ByteOrder.nativeOrder());         imageBuffer.put(data, 0, data.length);     }     catch (Exception e)     {         throw new TextureFormatException("Unable to convert data  for texture " + e);     }     return imageBuffer; } 

At last we have the ByteBuffer correctly formatted for OpenGL. All that is left is to get a bind ID and hand over the data to OpenGL. Everything we’ve done until now could have been done offline—that is, before OpenGL is configured, initialized, and rendered—and is recommended when possible. Loading and converting images in the middle of an OpenGL render cycle will inevitably decrease runtime performance.

To get a bind ID as well as to pass over the texture data to OpenGL, we will need a valid GL reference. Also, it must be done within the thread that is assigned to the OpenGL context. Often that means the texture binding will be done in the init() method of GLEventListener, but if it happens later on during execution, it will most likely be inside the display() method. In any case we must have a live and current GL object. The JOGL bind ID calls are straight, standard OpenGL texture commands with the exception of the int[] handle for the bind and ByteBuffer syntax. Putting it all together would look something like the following code:

static public int createTexture(String name,                              String resourceName,                              int target,                              int dstPixelFormat,                              int minFilter,                              int magFilter,                              boolean wrap,                              boolean mipmapped) throws  IOException, TextureFormatException {     // create the texture ID for this texture     //     int[] tmp = new int[1];     gl.glGenTextures(1, tmp);     int textureID = tmp[0];     // bind this texture     //     gl.glBindTexture(GL.GL_TEXTURE_2D, textureID);     // load the buffered image for this resource - save a copy so      we can draw into it later     //     BufferedImage bufferedImage = loadImage(resourceName);     int srcPixelFormat;     if (bufferedImage.getColorModel().hasAlpha())     {         srcPixelFormat = GL.GL_RGBA;     }     else     {         srcPixelFormat = GL.GL_RGB;     }     // convert that image into a byte buffer of texture data     //     ByteBuffer textureBuffer = convertImageData(bufferedImage);     // set up the texture wrapping mode depending on whether      // this texture is specified for wrapping or not     //     int wrapMode = wrap ? GL.GL_REPEAT : GL.GL_CLAMP;     if (target == GL.GL_TEXTURE_2D)     {         gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_S,         wrapMode);         gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_T,         wrapMode);         gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER,         minFilter);         gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER,         magFilter);     }     // create either a series of mipmaps or a single texture image      // based on what’s loaded     //     if (mipmapped)     {         glu.gluBuild2DMipmaps(target,                 dstPixelFormat,                 bufferedImage.getWidth(),                 bufferedImage.getHeight(),                 srcPixelFormat,                 GL.GL_UNSIGNED_BYTE,                 textureBuffer);     }     else     {         gl.glTexImage2D(target,                 0,                 dstPixelFormat,                 bufferedImage.getWidth(),                 bufferedImage.getHeight(),                 0,                 srcPixelFormat,                 GL.GL_UNSIGNED_BYTE,                 textureBuffer);     }     return textureID; }

After this, the OpenGL textures and IDs are properly set up and ready to use.

Vertex Arrays and ByteBuffers

ByteBuffers are also the mechanism used to set up vertex arrays in JOGL. Whereas OpenGL takes C float-array pointers for vertex arrays using the following function:

void glVertexPointer(GLint size,                      GLenum type,                      GLsizei stride,                      const GLvoid *pointer)

Similar to texture ByteBuffers, the JOGL method is:

public void glVertexPointer(int size,                             int type,                             int stride,                             Buffer ptr)

The easiest way to use vertex arrays is to make a regular Java float[] array containing the appropriate vertex, color, normal, or texture coordinate data and pass the array into a ByteBuffer in the array put() call. A simple triangle vertex array for a unit square follows:

float[] verts = new float[] {      -1.0f,  1.0f, 0.0f,     -1.0f, -1.0f, 0.0f,      1.0f, -1.0f, 0.0f,     -1.0f,  1.0f, 0.0f,      1.0f, -1.0f, 0.0f,      1.0f,  1.0f, 0.0f; }; FloatBuffer vertexArray = ByteBuffer.allocateDirect(verts.length*4).order (ByteOrder.nativeOrder()).asFloatBuffer(); vertexArray.put( verts );

Getting it to render is nearly identical to standard OpenGL:

gl.glVertexPointer(3, GL.GL_FLOAT, 0, vertexArray); gl.glEnableClientState(GL.GL_VERTEX_ARRAY); gl.glDrawArrays(GL.GL_TRIANGLES, 0, vertexArray.capacity());

Multithreading

Multithreading is another issue that creates some difficulties for developers first working with JOGL. Usually this step occurs in basic GUI test applications that aren’t designed around the single-threaded nature of OpenGL. Most errors happen when the user sets up some GUI components such as a button, wants to catch that button’s press-action event, and then wants the event action to do some direct OpenGL state setting.

For example, the developer might wish to have a GUI button enable or disable texturing in the OpenGL renderer. The developer makes the GL instance object available to the anonymous event Listener object, either by a static reference or through an accessor method, and then proceeds to call that GL object’s glEnable/Disable() on whatever state they want to affect. Unfortunately, this is not going to work because the GUI event thread now performing the GL call is not the assigned rendering thread. This is a violation of the threaded access to OpenGL and at best will result in no change or possibly a runtime exception, and at worst an application or system crash.

Two popular solutions are available for this problem. One is to have the desired modifiable states in OpenGL declared as Java class variables that are used to modify the OpenGL render states using the assigned render thread when display() is called.

The second is to set up a messaging system, where messages or requests for OpenGL changes are made and queued up until the next display() is called when the rendering thread reads though the messages performing the requested OpenGL operations. This is a typical design pattern in Swing and other GUI apps.

Either design is acceptable, but no matter which way multithreading is managed, it is likely that this issue will need to be addressed in all but the simplest of JOGL applications.



Practical Java Game Programming
Practical Java Game Programming (Charles River Media Game Development)
ISBN: 1584503262
EAN: 2147483647
Year: 2003
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net