CS 424: Computer Graphics, Fall 2017
Lab 7: Textures

In this lab, you will work on a program that applies image textures to 3D objects. The image texture can come from a resource file in the program. To add some interest, it is also possible to draw an image by hand and use that image as a texture, and it is possible to take a copy of the OpenGL image and draw on top of that by hand. The program window has two panels, one on the left for drawing and one on the right for displaying an OpenGL scene that shows one 3D object. For the cylinder shown on the right, I drew cartoon face, used it as a texture on a cube, copied the cube image to the paint panel, drew some red curves around the cube, and finally used that image as a texture on a cylinder.

For this lab, you will have to work in Java. Textures support in JOGL is quite different from the C API. Since you might want to use textures in your midterm project, you should learn the JOGL version first. (If you do decide to support textures in the project, I suggest that your API should have a method to create a texture from a BufferedImage, as is done in this lab. Leave the problem of getting the image into a BufferedImage to the program that uses your API.)

Before the lab, you should have read Section 4.3 in the textbook.

You will need copies of all seven files in the folder /classes/cs424/lab7-files. There are four Java files and three image files. The main program is Lab7.java, and that is the only file on which you will work. The other files are used by Lab7.java: PaintPanel.java defines the panel that is used for drawing (and most of the menus); Camera.java defines a camera class that is used in the program to let the user rotate the 3D object by dragging with the mouse; and TexturedShapes.java defines all but one of the 3D shapes that are used in the program. Copy the seven files into an Eclipse project that is set up to use JOGL.

This lab is due next Thursday, October 19. I will look for Lab7.java somewhere in your homework folder, inside /classes/cs424/homework.

Create Textures

The OpenGL API does not include functions for loading an image texture from an image file. (You have to load the texture from raw pixel color data in memory.) However, JOGL does have methods for creating image textures from image files and from BufferedImages. The image textures are represented as objects of type Texture, one of the classes included in the JOGL API.

For this lab, all of the the textures are created from BufferedImages. The textures are created in two methods, textureFromPainting() and textureFromResource(), which you need to complete (mostly to get you to pay attention to the process). These methods are called by the ActionListerner that handles events from the "Texture" menu. To create the textures, you will use some methods from JOGL's texture API.

But of course, there is a problem: The Jogl methods need access to a GL2 graphics context. (In some cases, the OpenGL context is used internally, even though it is not passed as a parameter, and the method will throw an exception if no OpenGL context is available.) The way that JOGL works is that for OpenGL methods to work, an OpenGL context must be "made current on the current thread." Before calling methods such as display() and init(), the JOGL system automatically makes the appropriate OpenGL context current. However, the methods textureFromPaint() and textureFromResource() will be called by the event-handling thread, not by the JOGL system, and the OpenGL context presumably won't be current when they are called. This means that you need to make the OpenGL context for the panel, displayGL, current before you create the texture. Furthermore, you must then release the context before returning from the method, but only if the context was not already current when the method was called. This is already done, as an example, in another, complete method, paintingFromOpenGL(), so that you can see how it's done. Here is the relevant code from that method, which you can copy into textureFromPaint() and textureFromResource():

GLContext context = displayGL.getContext(); // Context for display panel.
boolean needsRelease = false; 
if ( ! context.isCurrent() ) {
        // Make the context current on the current thread.
    context.makeCurrent();
    needsRelease = true;
}
GL2 gl2 = context.getGL().getGL2();
   .
   .   // code that uses gl2!
   .
if (needsRelease) {
    context.release();
}

Now to compete the two methods... First, for each method, you need to get the BufferedImage that will be used as the source for the texture image. For textureFromPainting(), the texture is created from a BufferedImage containing a copy of the drawing from the paint panel. You can get the BufferedImage by calling paintPanel.copyOSC(), which returns a copy of the image from that panel.

For textureFromResource(), the BufferedImage must be loaded from a resource file. Getting an image from a resource is standard stuff (see for example Section 13.1.3 in my Java textbook):

URL textureURL;
textureURL = this.getClass().getClassLoader().getResource( resourceName );
BufferedImage img = ImageIO.read( textureURL );

Here, the resouceName is just the name of the image resource file (or a path to the resource, if it is in a non-default package). This code can throw checked exceptions, which must be handled. (Since a resource is included as part of the program, any exception would be a bug in the program.)

Once you have the buffered image, img, a Texture can be created from img using the static method AWTTextureIO.newTexture(). Here is the code that you need; it also sets the texture wrap modes, since GL_REPEAT is not the default in JOGL (even though it is the default in OpenGL). Here, texture is a variable of type Texture:

ImageUtil.flipImageVertically( img );
texture = AWTTextureIO.newTexture(displayGL.getGLProfile(), img, true);
texture.setTexParameteri(gl2, GL2.GL_TEXTURE_WRAP_S, GL2.GL_REPEAT);
texture.setTexParameteri(gl2, GL2.GL_TEXTURE_WRAP_T, GL2.GL_REPEAT);

Use Textures on Graphical Objects

OpenGL will attempt to apply a 2D image texture to a primitive if GL_TEXTURE_2D is enabled when the primitive is drawn. For drawing a primitive with no texture, GL_TEXTURE_2D must be disabled. 2D textures can be enabled and disabled by calling

gl2.glEnable( GL2.GL_TEXTURE_2D );

gl2.glDisable( GL2.GL_TEXTURE_2D );

To specify which texture to use on the primitive, you have to bind the texture. The currently bound texture when a graphical object is rendered is the one that will be used on that object. For a JOGL Texture, tex, the texture that it represents can be bound by calling

tex.bind(gl2);

The program Lab7.java has a variable currentTexture of type Texture that holds the texture image that should be applied to the 3D object. The value of the variable is null if no texture is to be applied.

Add a few lines of code to the display() method to enable and bind the current texture, or to disable texturing if the current texture is null. Once you have done that (and completed the previous part of the lab), you should be able to see textures on all of the objects except for the Triangular Prism.

Texture Coordinates for the Prism

The last object in the "3D Object" menu is a "Triangular Prism." While the other shapes are drawn by methods from TexturedObjects.java, or from GLUT in the case of the teapot, the prism is drawn by the method triangularPrism() in the main file, Lab7.java. In that method, each side is drawn as a separate OpenGL primitive, with an appropriate normal vector. However, no texture coordinates are provided for the vertices of the primitive. (You might look at the prism with several different textures applied—say, Earth and Brick—before defining texture coordinates, and see if you can figure out where the color comes from.)

The coordinates for the vertices of the prism are shown on the left, below. (The value of t is Math.sqrt(3)/4, which gives an equilateral triangle for the base.) You should provide texture coordinates for each vertex in the primitives. The texture must wrap once around the sides of the prism. That is, you should use a different 1/3 of the texture for each side, and the pieces should meet up nicely at the edges between the sides. For the top and bottom of the prism, you can cut out any triangular area from the texture. The prism with the Earth texture is shown on the right:

It will require some care to get the texture coordinates correct. Plan before you begin! It might be helpful to write out the texture coordinates on the drawing of the prism. Also, I suggest that you work on the front left face first, and get that right before working on the other two sides.

Texture Transformation

Finally, you should implement a texture transformation in the display() method, before the textured object is drawn. The texture transform in this program is used to implement the "texture repeat" commands in the "Texture" menu. Setting a horizontal texture repeat to N means that the texture should be repeated N times horizontally in the space where only one copy of the texture would normally be. This can be implemented by a texture transformation that scales the horizontal texture coordinates by a factor of N. Vertical texture repeat works in the same way. Texture transformations can be used to better adapt an image to a surface when the surface has texture coordinates that are not appropriate for the texture image. For example, the "Brick" texture fits the torus better with horizontal repeat set to 4 and vertical repeat set to 2:

The repeat factors in the program are stored in global integer variables textureRepeatHorizontal and textureRepeatVertical. These variables are already set by the commands in the "Texture" menu. You just need to apply them in the display() method as the horizontal and vertical scale factors in a texture transformation.

The texture transformation in OpenGL 1.1, just like the projection and modelview transformations, is stored as a matrix, and you work on that matrix with the same functions that you use for modelview transformations (glRotate*, glScale*, glTranslate*, and glLoadIdentity). To work on the texture transformation matrix, you must set the matrix mode to GL2.GL_TEXTURE by calling

gl2.glMatrixMode(GL2.GL_TEXTURE);

Remember to set the matrix mode back to GL2.GL_MODELVIEW after setting up the texture transform!