diff options
Diffstat (limited to 'src/classes/com/sun/opengl/utils/TextureIO.java')
-rwxr-xr-x | src/classes/com/sun/opengl/utils/TextureIO.java | 454 |
1 files changed, 420 insertions, 34 deletions
diff --git a/src/classes/com/sun/opengl/utils/TextureIO.java b/src/classes/com/sun/opengl/utils/TextureIO.java index 07bb67ce4..d1f62cd5c 100755 --- a/src/classes/com/sun/opengl/utils/TextureIO.java +++ b/src/classes/com/sun/opengl/utils/TextureIO.java @@ -39,6 +39,7 @@ package com.sun.opengl.utils; +import java.awt.Graphics; import java.awt.image.*; import java.io.*; import java.net.*; @@ -47,25 +48,28 @@ import java.util.*; import javax.imageio.*; import javax.media.opengl.*; +import javax.media.opengl.glu.*; /** <P> Provides input and output facilities for both loading OpenGL textures from disk and streams as well as writing textures already in memory back to disk. </P> <P> The TextureIO class supports an arbitrary number of plug-in - TextureProviders which know how to produce TextureData objects - from files, InputStreams and URLs. The TextureData class - represents the raw data of the texture before it has been - converted to an OpenGL texture object. The Texture class + readers and writers via TextureProviders and TextureWriters. + TextureProviders know how to produce TextureData objects from + files, InputStreams and URLs. TextureWriters know how to write + TextureData objects to disk in various file formats. The + TextureData class represents the raw data of the texture before it + has been converted to an OpenGL texture object. The Texture class represents the OpenGL texture object and provides easy facilities for using the texture. </P> - <P> There are several built-in TextureProviders supplied with the - TextureIO implementation. The most basic provider uses the - platform's Image I/O facilities to read in a BufferedImage and - convert it to a texture. This is the baseline provider and is - registered so that it is the last one consulted. All others are - asked first to open a given file. </P> + <P> There are several built-in TextureProviders and TextureWriters + supplied with the TextureIO implementation. The most basic + provider uses the platform's Image I/O facilities to read in a + BufferedImage and convert it to a texture. This is the baseline + provider and is registered so that it is the last one consulted. + All others are asked first to open a given file. </P> <P> There are three other providers registered by default as of the time of this writing. One handles SGI RGB (".sgi", ".rgb") @@ -84,6 +88,18 @@ import javax.media.opengl.*; when probing for e.g. magic numbers at the head of the file to make sure not to disturb the state of the InputStream for downstream TextureProviders. </P> + + <P> There are analogous TextureWriters provided for writing + textures back to disk if desired. As of this writing, there are + four TextureWriters registered by default: one for Targa files, + one for SGI RGB files, one for DirectDraw surface (.dds) files, + and one for ImageIO-supplied formats such as .jpg and .png. Some + of these writers have certain limitations such as only being able + to write out textures stored in GL_RGB or GL_RGBA format. The DDS + writer supports fetching and writing to disk of texture data in + DXTn compressed format. Whether this will occur is dependent on + whether the texture's internal format is one of the DXTn + compressed formats and whether the target file is .dds format. */ public class TextureIO { @@ -396,10 +412,11 @@ public class TextureIO { * @param data the texture data to turn into an OpenGL texture * @throws GLException if no OpenGL context is current or if an * OpenGL error occurred + * @throws IllegalArgumentException if the passed TextureData was null */ - public static Texture newTexture(TextureData data) throws GLException { + public static Texture newTexture(TextureData data) throws GLException, IllegalArgumentException { if (data == null) { - return null; + throw new IllegalArgumentException("Null TextureData"); } return new Texture(data); } @@ -513,6 +530,120 @@ public class TextureIO { return new Texture(target); } + /** + * Writes the given texture to a file. The type of the file is + * inferred from its suffix. An OpenGL context must be current in + * order to fetch the texture data back from the OpenGL pipeline. + * This method causes the specified Texture to be bound to the + * GL_TEXTURE_2D state. If no suitable writer for the requested file + * format was found, throws an IOException. <P> + * + * Reasonable attempts are made to produce good results in the + * resulting images. The Targa, SGI and ImageIO writers produce + * results in the correct vertical orientation for those file + * formats. The DDS writer performs no vertical flip of the data, + * even in uncompressed mode. (It is impossible to perform such a + * vertical flip with compressed data.) Applications should keep + * this in mind when using this routine to save textures to disk for + * later re-loading. <P> + * + * Any mipmaps for the specified texture are currently discarded + * when it is written to disk, regardless of whether the underlying + * file format supports multiple mipmaps in a given file. + * + * @throws IOException if an error occurred during writing or no + * suitable writer was found + * @throws GLException if no OpenGL context was current or an + * OpenGL-related error occurred + */ + public static void write(Texture texture, File file) throws IOException, GLException { + if (texture.getTarget() != GL.GL_TEXTURE_2D) { + throw new GLException("Only GL_TEXTURE_2D textures are supported"); + } + + // First fetch the texture data + GL gl = GLU.getCurrentGL(); + + texture.bind(); + int internalFormat = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_INTERNAL_FORMAT); + int width = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_WIDTH); + int height = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_HEIGHT); + int border = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_BORDER); + TextureData data = null; + if (internalFormat == GL.GL_COMPRESSED_RGB_S3TC_DXT1_EXT || + internalFormat == GL.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT || + internalFormat == GL.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT || + internalFormat == GL.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT) { + // Fetch using glGetCompressedTexImage + int size = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_COMPRESSED_IMAGE_SIZE); + ByteBuffer res = ByteBuffer.allocate(size); + gl.glGetCompressedTexImage(GL.GL_TEXTURE_2D, 0, res); + data = new TextureData(internalFormat, width, height, border, internalFormat, GL.GL_UNSIGNED_BYTE, + false, true, true, res, null); + } else { + int bytesPerPixel = 0; + int fetchedFormat = 0; + switch (internalFormat) { + case GL.GL_RGB: + case GL.GL_BGR: + case GL.GL_RGB8: + bytesPerPixel = 3; + fetchedFormat = GL.GL_RGB; + break; + case GL.GL_RGBA: + case GL.GL_BGRA: + case GL.GL_ABGR_EXT: + case GL.GL_RGBA8: + bytesPerPixel = 4; + fetchedFormat = GL.GL_RGBA; + break; + default: + throw new IOException("Unsupported texture internal format 0x" + Integer.toHexString(internalFormat)); + } + + // Fetch using glGetTexImage + int packAlignment = glGetInteger(GL.GL_PACK_ALIGNMENT); + int packRowLength = glGetInteger(GL.GL_PACK_ROW_LENGTH); + int packSkipRows = glGetInteger(GL.GL_PACK_SKIP_ROWS); + int packSkipPixels = glGetInteger(GL.GL_PACK_SKIP_PIXELS); + int packSwapBytes = glGetInteger(GL.GL_PACK_SWAP_BYTES); + + gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, 1); + gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, 0); + gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS, 0); + gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, 0); + gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, 0); + + ByteBuffer res = ByteBuffer.allocate((width + (2 * border)) * + (height + (2 * border)) * + bytesPerPixel); + System.err.println("Allocated buffer of size " + res.remaining() + " for fetched image (" + + ((fetchedFormat == GL.GL_RGB) ? "GL_RGB" : "GL_RGBA") + ")"); + gl.glGetTexImage(GL.GL_TEXTURE_2D, 0, fetchedFormat, GL.GL_UNSIGNED_BYTE, res); + + gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, packAlignment); + gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, packRowLength); + gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS, packSkipRows); + gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, packSkipPixels); + gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, packSwapBytes); + + data = new TextureData(internalFormat, width, height, border, fetchedFormat, GL.GL_UNSIGNED_BYTE, + false, false, false, res, null); + + System.out.println("data.getPixelFormat() = " + + ((data.getPixelFormat() == GL.GL_RGB) ? "GL_RGB" : "GL_RGBA")); + } + + for (Iterator iter = textureWriters.iterator(); iter.hasNext(); ) { + TextureWriter writer = (TextureWriter) iter.next(); + if (writer.write(file, data)) { + return; + } + } + + throw new IOException("No suitable texture writer found"); + } + //---------------------------------------------------------------------- // Helper function for above TextureProviders /** @@ -540,8 +671,25 @@ public class TextureIO { return toLowerCase(filename.substring(lastDot + 1)); } - // FIXME: add texture writing capabilities - // public void writeTextureToFile(Texture texture, File file, boolean saveUncompressed) throws IOException, GLException; + //---------------------------------------------------------------------- + // Helper function which may be more generally useful + // + + /** Flips the supplied BufferedImage vertically. This is often a + necessary conversion step to display a Java2D image correctly + with OpenGL and vice versa. */ + public static void flipImageVertically(BufferedImage image) { + WritableRaster raster = image.getRaster(); + Object scanline1 = null; + Object scanline2 = null; + + for (int i = 0; i < image.getHeight() / 2; i++) { + scanline1 = raster.getDataElements(0, i, image.getWidth(), 1, scanline1); + scanline2 = raster.getDataElements(0, image.getHeight() - i - 1, image.getWidth(), 1, scanline2); + raster.setDataElements(0, i, image.getWidth(), 1, scanline2); + raster.setDataElements(0, image.getHeight() - i - 1, image.getWidth(), 1, scanline1); + } + } //---------------------------------------------------------------------- // SPI support @@ -556,11 +704,21 @@ public class TextureIO { textureProviders.add(0, provider); } + /** Adds a TextureWriter to support writing of a new file + format. */ + public static void addTextureWriter(TextureWriter writer) { + // Must always add at the front so the ImageIO writer is last, + // so we don't accidentally use it instead of a user's possibly + // more optimal writer + textureWriters.add(0, writer); + } + //---------------------------------------------------------------------- // Internals only below this point // private static List/*<TextureProvider>*/ textureProviders = new ArrayList/*<TextureProvider>*/(); + private static List/*<TextureWriter>*/ textureWriters = new ArrayList/*<TextureWriter>*/(); static { // ImageIO provider, the fall-back, must be the first one added @@ -570,6 +728,14 @@ public class TextureIO { addTextureProvider(new DDSTextureProvider()); addTextureProvider(new SGITextureProvider()); addTextureProvider(new TGATextureProvider()); + + // ImageIO writer, the fall-back, must be the first one added + textureWriters.add(new IIOTextureWriter()); + + // Other special-case writers + addTextureWriter(new DDSTextureWriter()); + addTextureWriter(new SGITextureWriter()); + addTextureWriter(new TGATextureWriter()); } // Implementation methods @@ -595,7 +761,8 @@ public class TextureIO { return data; } } - return null; + + throw new IOException("No suitable reader for given file"); } private static TextureData newTextureDataImpl(InputStream stream, @@ -626,7 +793,7 @@ public class TextureIO { } } - return null; + throw new IOException("No suitable reader for given stream"); } private static TextureData newTextureDataImpl(URL url, @@ -652,7 +819,7 @@ public class TextureIO { } } - return null; + throw new IOException("No suitable reader for given URL"); } private static TextureData newTextureDataImpl(BufferedImage image, @@ -713,12 +880,11 @@ public class TextureIO { String fileSuffix) throws IOException { if (DDS.equals(fileSuffix) || DDS.equals(getFileSuffix(file))) { - final DDSReader reader = new DDSReader(); - reader.loadFile(file); - DDSReader.ImageInfo info = reader.getMipMap(0); + final DDSImage image = DDSImage.read(file); + DDSImage.ImageInfo info = image.getMipMap(0); if (pixelFormat == 0) { - switch (reader.getPixelFormat()) { - case DDSReader.D3DFMT_R8G8B8: + switch (image.getPixelFormat()) { + case DDSImage.D3DFMT_R8G8B8: pixelFormat = GL.GL_RGB; break; default: @@ -728,23 +894,23 @@ public class TextureIO { } if (info.isCompressed()) { switch (info.getCompressionFormat()) { - case DDSReader.D3DFMT_DXT1: + case DDSImage.D3DFMT_DXT1: internalFormat = GL.GL_COMPRESSED_RGB_S3TC_DXT1_EXT; break; - case DDSReader.D3DFMT_DXT3: + case DDSImage.D3DFMT_DXT3: internalFormat = GL.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; break; - case DDSReader.D3DFMT_DXT5: + case DDSImage.D3DFMT_DXT5: internalFormat = GL.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; default: throw new RuntimeException("Unsupported DDS compression format \"" + - DDSReader.getCompressionFormatName(info.getCompressionFormat()) + "\""); + DDSImage.getCompressionFormatName(info.getCompressionFormat()) + "\""); } } if (internalFormat == 0) { - switch (reader.getPixelFormat()) { - case DDSReader.D3DFMT_R8G8B8: + switch (image.getPixelFormat()) { + case DDSImage.D3DFMT_R8G8B8: pixelFormat = GL.GL_RGB; break; default: @@ -754,14 +920,14 @@ public class TextureIO { } TextureData.Flusher flusher = new TextureData.Flusher() { public void flush() { - reader.close(); + image.close(); } }; TextureData data; - if (mipmap && reader.getNumMipMaps() > 0) { - Buffer[] mipmapData = new Buffer[reader.getNumMipMaps()]; - for (int i = 0; i < reader.getNumMipMaps(); i++) { - mipmapData[i] = reader.getMipMap(i).getData(); + if (mipmap && image.getNumMipMaps() > 0) { + Buffer[] mipmapData = new Buffer[image.getNumMipMaps()]; + for (int i = 0; i < image.getNumMipMaps(); i++) { + mipmapData[i] = image.getMipMap(i).getData(); } data = new TextureData(internalFormat, info.getWidth(), @@ -909,7 +1075,7 @@ public class TextureIO { mipmap, false, false, - ByteBuffer.wrap(image.getData()), + image.getData(), null); } @@ -918,9 +1084,229 @@ public class TextureIO { } //---------------------------------------------------------------------- + // ImageIO texture writer + // + static class IIOTextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if ((pixelFormat == GL.GL_RGB || + pixelFormat == GL.GL_RGBA) && + (pixelType == GL.GL_BYTE || + pixelType == GL.GL_UNSIGNED_BYTE)) { + // Convert TextureData to appropriate BufferedImage + // FIXME: almost certainly not obeying correct pixel order + BufferedImage image = new BufferedImage(data.getWidth(), data.getHeight(), + (pixelFormat == GL.GL_RGB) ? + BufferedImage.TYPE_3BYTE_BGR : + BufferedImage.TYPE_4BYTE_ABGR); + byte[] imageData = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + ByteBuffer buf = (ByteBuffer) data.getBuffer(); + if (buf == null) { + buf = (ByteBuffer) data.getMipmapData()[0]; + } + buf.rewind(); + buf.get(imageData); + buf.rewind(); + + // Swizzle image components to be correct + if (pixelFormat == GL.GL_RGB) { + for (int i = 0; i < imageData.length; i += 3) { + byte red = imageData[i + 0]; + byte blue = imageData[i + 2]; + imageData[i + 0] = blue; + imageData[i + 2] = red; + } + } else { + for (int i = 0; i < imageData.length; i += 4) { + byte red = imageData[i + 0]; + byte green = imageData[i + 1]; + byte blue = imageData[i + 2]; + byte alpha = imageData[i + 3]; + imageData[i + 0] = alpha; + imageData[i + 1] = blue; + imageData[i + 2] = green; + imageData[i + 3] = red; + } + } + + // Flip image vertically for the user's convenience + flipImageVertically(image); + + // Happened to notice that writing RGBA images to JPEGS is broken + if (JPG.equals(getFileSuffix(file)) && + image.getType() == BufferedImage.TYPE_4BYTE_ABGR) { + BufferedImage tmpImage = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); + Graphics g = tmpImage.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + image = tmpImage; + } + + return ImageIO.write(image, getFileSuffix(file), file); + } + + throw new IOException("ImageIO writer doesn't support this pixel format / type (only GL_RGB/A + bytes)"); + } + } + + //---------------------------------------------------------------------- + // DDS texture writer + // + static class DDSTextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + if (DDS.equals(getFileSuffix(file))) { + // See whether the DDS writer can handle this TextureData + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if (pixelType != GL.GL_BYTE && + pixelType != GL.GL_UNSIGNED_BYTE) { + throw new IOException("DDS writer only supports byte / unsigned byte textures"); + } + + int d3dFormat = 0; + // FIXME: some of these are probably not completely correct and would require swizzling + switch (pixelFormat) { + case GL.GL_RGB: d3dFormat = DDSImage.D3DFMT_R8G8B8; break; + case GL.GL_RGBA: d3dFormat = DDSImage.D3DFMT_A8R8G8B8; break; + case GL.GL_COMPRESSED_RGB_S3TC_DXT1_EXT: d3dFormat = DDSImage.D3DFMT_DXT1; break; + case GL.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: throw new IOException("RGBA DXT1 not yet supported"); + case GL.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: d3dFormat = DDSImage.D3DFMT_DXT3; break; + case GL.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: d3dFormat = DDSImage.D3DFMT_DXT5; break; + default: throw new IOException("Unsupported pixel format 0x" + Integer.toHexString(pixelFormat) + " by DDS writer"); + } + + ByteBuffer[] mipmaps = null; + if (data.getMipmapData() != null) { + mipmaps = new ByteBuffer[data.getMipmapData().length]; + for (int i = 0; i < mipmaps.length; i++) { + mipmaps[i] = (ByteBuffer) data.getMipmapData()[i]; + } + } else { + mipmaps = new ByteBuffer[] { (ByteBuffer) data.getBuffer() }; + } + + DDSImage image = DDSImage.createFromData(d3dFormat, + data.getWidth(), + data.getHeight(), + mipmaps); + image.write(file); + return true; + } + + return false; + } + } + + //---------------------------------------------------------------------- + // SGI (rgb) texture writer + // + static class SGITextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + String fileSuffix = getFileSuffix(file); + if (SGI.equals(fileSuffix) || + SGI_RGB.equals(fileSuffix)) { + // See whether the SGI writer can handle this TextureData + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if ((pixelFormat == GL.GL_RGB || + pixelFormat == GL.GL_RGBA) && + (pixelType == GL.GL_BYTE || + pixelType == GL.GL_UNSIGNED_BYTE)) { + ByteBuffer buf = ((data.getBuffer() != null) ? + (ByteBuffer) data.getBuffer() : + (ByteBuffer) data.getMipmapData()[0]); + byte[] bytes; + if (buf.hasArray()) { + bytes = buf.array(); + } else { + buf.rewind(); + bytes = new byte[buf.remaining()]; + buf.get(bytes); + buf.rewind(); + } + + SGIImage image = SGIImage.createFromData(data.getWidth(), + data.getHeight(), + (pixelFormat == GL.GL_RGBA), + bytes); + image.write(file, false); + return true; + } + + throw new IOException("SGI writer doesn't support this pixel format / type (only GL_RGB/A + bytes)"); + } + + return false; + } + } + + //---------------------------------------------------------------------- + // TGA (Targa) texture writer + + static class TGATextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + if (TGA.equals(getFileSuffix(file))) { + // See whether the TGA writer can handle this TextureData + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if ((pixelFormat == GL.GL_RGB || + pixelFormat == GL.GL_RGBA) && + (pixelType == GL.GL_BYTE || + pixelType == GL.GL_UNSIGNED_BYTE)) { + ByteBuffer buf = ((data.getBuffer() != null) ? + (ByteBuffer) data.getBuffer() : + (ByteBuffer) data.getMipmapData()[0]); + // Must reverse order of red and blue channels to get correct results + int skip = ((pixelFormat == GL.GL_RGB) ? 3 : 4); + for (int i = 0; i < buf.remaining(); i += skip) { + byte red = buf.get(i + 0); + byte blue = buf.get(i + 2); + buf.put(i + 0, blue); + buf.put(i + 2, red); + } + + TGAImage image = TGAImage.createFromData(data.getWidth(), + data.getHeight(), + (pixelFormat == GL.GL_RGBA), + false, + ((data.getBuffer() != null) ? + (ByteBuffer) data.getBuffer() : + (ByteBuffer) data.getMipmapData()[0])); + image.write(file); + return true; + } + + throw new IOException("TGA writer doesn't support this pixel format / type (only GL_RGB/A + bytes)"); + } + + return false; + } + } + + //---------------------------------------------------------------------- // Helper routines // + private static int glGetInteger(int pname) { + int[] tmp = new int[1]; + GL gl = GLU.getCurrentGL(); + gl.glGetIntegerv(pname, tmp, 0); + return tmp[0]; + } + + private static int glGetTexLevelParameteri(int target, int level, int pname) { + int[] tmp = new int[1]; + GL gl = GLU.getCurrentGL(); + gl.glGetTexLevelParameteriv(target, 0, pname, tmp, 0); + return tmp[0]; + } + private static String toLowerCase(String arg) { if (arg == null) { return null; |