Hi!
What you see here is a texture compressor/converter, that is independent from other parts of the engine, and is universaly usable. Lots of you may know the DDS file format. The DDS file format has the texture compressed in the way that the GPU can directly use, without the need of any preprocessing. For creating DDS files, you can use different tools, but there is a feature in OpenGL that makes it possible to create compressed textures without the need of any external tool. What i have here is a Java class that takes an uncompressed texture image and:
- Resizes it to power-of-two
- Generates mipmaps
- Compresses it to DXT compression format
- Saves it to a new Image object
This Image object can then be saved to disk, and loaded from disk when it is needed afterwards. It makes a huge difference when textures are loaded from format already usable by OpenGL. In the case of application i am working on, the loading time has dropped 4 times with loading pre-compressed textures instead of loading PNG textures.
What i wont post here, is the resource-management part, which is dependant on the application. Note that this code is not much usable as-is, but you can use this code as the base to implement the texture conversion/compression in your own application. If jME devs choose so, it can be included in the core jME too.
This converter is ment to be used the following ways:
- When you package your application for distribution, you do batch conversion from PNG, JPG, or whatever format to DXT compressed textures, then distribute those textures.
- When your application downloads an update of the textures, it downloads in original format (PNG, …) but saves converted textures to a local cache folder.
- When the application needs a texture for rendering, it should load converted textures and not the original ones.
You will have to fix the imports, i used different namespace.
The TextureConverter.java:
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import org.lwjgl.opengl.ARBTextureCompression;
import org.lwjgl.opengl.EXTTextureCompressionS3TC;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.Util;
import org.lwjgl.opengl.glu.GLU;
import org.lwjgl.opengl.glu.MipMap;
/**
* This class takes an uncompressed image and compresses it using
* OpenGL. This conversion should be
* done by the developer in a batch-processing, or during installing
* and not during the actual rendering.
*
* Lots of code taken fron LWJGLTextureState
* @author vear (Arpad Vekas)
*/
public class TextureConverter {
// the input image componenets we handle
private static int[] imageComponents = {
GL11.GL_RGBA4,
GL11.GL_RGB8,
GL11.GL_RGB5_A1,
GL11.GL_RGBA8,
GL11.GL_LUMINANCE8_ALPHA8,
EXTTextureCompressionS3TC.GL_COMPRESSED_RGB_S3TC_DXT1_EXT,
EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT,
EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT,
EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT,
GL11.GL_LUMINANCE16 };
// the corresponding output compressed format
private static int[] compressedComponents = {
ARBTextureCompression.GL_COMPRESSED_RGBA_ARB,
ARBTextureCompression.GL_COMPRESSED_RGB_ARB,
ARBTextureCompression.GL_COMPRESSED_RGBA_ARB,
ARBTextureCompression.GL_COMPRESSED_RGBA_ARB,
ARBTextureCompression.GL_COMPRESSED_LUMINANCE_ALPHA_ARB,
ARBTextureCompression.GL_COMPRESSED_RGB_ARB,
ARBTextureCompression.GL_COMPRESSED_RGBA_ARB,
ARBTextureCompression.GL_COMPRESSED_RGBA_ARB,
ARBTextureCompression.GL_COMPRESSED_RGBA_ARB,
ARBTextureCompression.GL_COMPRESSED_LUMINANCE_ARB };
private static int[] imageFormats = {
GL11.GL_RGBA,
GL11.GL_RGB,
GL11.GL_RGBA,
GL11.GL_RGBA,
GL11.GL_LUMINANCE_ALPHA,
GL11.GL_RGB,
GL11.GL_RGBA,
GL11.GL_RGBA,
GL11.GL_RGBA,
GL11.GL_LUMINANCE};
private static int[] nativeCompressed = {
EXTTextureCompressionS3TC.GL_COMPRESSED_RGB_S3TC_DXT1_EXT,
EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT,
EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT,
EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT
};
public TextureConverter() {};
public Image convertTexture(Image image) {
int imageHeight = image.getHeight();
int imageWidth = image.getWidth();
int imageType = image.getType();
IntBuffer res = BufferUtils.createIntBuffer(4);
res.clear();
GL11.glGenTextures(res);
int textureid = res.get(0);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureid);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
if(!FastMath.isPowerOfTwo(imageWidth)
|| !FastMath.isPowerOfTwo(imageHeight)) {
final int maxSize = LWJGLMipMap.glGetIntegerv(GL11.GL_MAX_TEXTURE_SIZE);
int w = LWJGLMipMap.nearestPower(imageWidth);
if (w > maxSize) {
w = maxSize;
}
int h = LWJGLMipMap.nearestPower(imageHeight);
if (h > maxSize) {
h = maxSize;
}
int format = LWJGLTextureState.getImageFormat(imageType);
int type = GL11.GL_UNSIGNED_BYTE;
int bpp = LWJGLMipMap.bytesPerPixel(format, type);
int size = (w + 4) * h * bpp;
ByteBuffer scaledImage = BufferUtils.createByteBuffer(size);
int error = MipMap.gluScaleImage(format, imageWidth,
imageHeight, type, image.getData(), w, h, type,
scaledImage);
if (error != 0) {
Util.checkGLError();
}
image.setWidth(w);
image.setHeight(h);
image.setData(scaledImage);
}
imageHeight = image.getHeight();
imageWidth = image.getWidth();
ByteBuffer data = image.getData();
// do the compression
GLU.gluBuild2DMipmaps(GL11.GL_TEXTURE_2D,
compressedComponents[imageType],
imageWidth,
imageHeight,
imageFormats[imageType],
GL11.GL_UNSIGNED_BYTE,
data);
// calculate the number of mips
int nummips = (int) FastMath.log2(FastMath.max(imageHeight, imageWidth)) +1;
// retrieve the compressed mips of the image
// create the array to hold data of the mips
int[] internal_format = new int[nummips];
int[] mip_size = new int[nummips];
ByteBuffer[] mip_data = new ByteBuffer[nummips];
int total_size = 0;
// for each mip level
for(int mip=0; mip<nummips; mip++) {
res.clear();
GL11.glGetTexLevelParameter(GL11.GL_TEXTURE_2D, mip, ARBTextureCompression.GL_TEXTURE_COMPRESSED_ARB, res);
res.rewind();
int compressed = res.get();
//if(compressed == GL11.GL_TRUE) {
// the compression was succesfull
res.clear();
GL11.glGetTexLevelParameter(GL11.GL_TEXTURE_2D, mip, GL11.GL_TEXTURE_INTERNAL_FORMAT, res);
res.rewind();
internal_format[mip] = res.get();
res.clear();
GL11.glGetTexLevelParameter(GL11.GL_TEXTURE_2D, mip,
ARBTextureCompression.GL_TEXTURE_COMPRESSED_IMAGE_SIZE_ARB,
res);
res.rewind();
mip_size[mip] = res.get();
if(mip_size[mip] > 0) {
total_size += mip_size[mip];
// allocate buffer for the mip
mip_data[mip] = BufferUtils.createByteBuffer(mip_size[mip]);
mip_data[mip].clear();
// get the compressed image data
ARBTextureCompression.glGetCompressedTexImageARB(GL11.GL_TEXTURE_2D, mip, mip_data[mip]);
}
//}
}
// release the texture
res.clear();
res.put(textureid);
res.rewind();
GL11.glDeleteTextures(res);
// save the compressed texture with all its mips into a file
// figure out the new compressed image type
int components = internal_format[0];
int format = 0;
for(int i=0; i<nativeCompressed.length; i++) {
if(components == nativeCompressed[i] ) {
// this is it
format = Image.LAST_UNCOMPRESSED_TYPE+1+i;
}
}
if(format == 0) {
// not found among compressed, try to find
// among normal ones
for(int i=0; i<imageComponents.length; i++) {
if(components == imageComponents[i] ) {
// this is it
format = i;
}
}
}
if(format==0) {
// other way to find out?
}
if(format!=0) {
// create a new image
Image img = new Image();
img.setHeight(imageHeight);
img.setWidth(imageWidth);
img.setType(format);
img.setMipMapSizes(mip_size);
// create a buffer to hold all the data
ByteBuffer img_data = BufferUtils.createByteBuffer(total_size);
// put all the mips in
img_data.clear();
for(int i=0;i<nummips; i++) {
if(mip_data[i]!=null) {
mip_data[i].rewind();
img_data.put(mip_data[i]);
}
}
img.setData(img_data);
return img;
}
// still not found, huh?
// TODO: save the OGL component type?
return null;
}
/**
* override MipMap to access helper methods
*/
protected static class LWJGLMipMap extends MipMap {
/**
* @see MipMap#glGetIntegerv(int)
*/
protected static int glGetIntegerv(int what) {
return org.lwjgl.opengl.glu.Util.glGetIntegerv(what);
}
/**
* @see MipMap#nearestPower(int)
*/
protected static int nearestPower(int value) {
return org.lwjgl.opengl.glu.Util.nearestPower(value);
}
/**
* @see MipMap#bytesPerPixel(int, int)
*/
protected static int bytesPerPixel(int format, int type) {
return org.lwjgl.opengl.glu.Util.bytesPerPixel(format, type);
}
}
}
After you have the new (converted) Image object, you can save it to a file easily. Here are the save() and load() methods i use for saving and loading of textures. If you have your own image saving/loading methods, just disregard the following code. If you take the time you could make a proper DDS file format save routine too.
/**
* Saves the given image to a ByteBuffer, which can later be
* saved to a file.
* @return The image data ready to be saved
*/
public ByteBuffer save() {
if(data==null)
return null;
// compute, how big a buffer we will need
int buffsize = 4 + //header
4 + //type
4 + //width
4 + //height
4 + //number of mips
( mipMapSizes == null ? 4 : mipMapSizes.length*4 ) + // size for each mip
data.limit(); // the total data buffer length
// create the buffer
DataBuffer saved = DataBuffer.allocate(buffsize);
saved.clear();
// header "VLT1"
saved.put("VLT1");
// type
saved.put(type);
// width
saved.put(width);
// height
saved.put(height);
// number of mips
saved.put(mipMapSizes == null ? 1 : mipMapSizes.length);
// for each mip, the size of the mip in bytes
if(mipMapSizes == null || mipMapSizes.length == 1) {
// if only one mip, the put in the data lenght
saved.put(data.limit());
} else {
// put in all the mip sizes
for(int i=0; i<mipMapSizes.length; i++)
saved.put(mipMapSizes[i]);
}
// image data
data.rewind();
saved.put(data);
return saved.getData();
}
/**
* Loads an Image object from the given buffer.
* @param buf The buffer containing the image data
*/
public boolean load(ByteBuffer buf) {
// create a DataBuffer
DataBuffer load = new DataBuffer();
load.setData(buf);
load.rewind();
// read in the header
if(!"VLT1".equals(load.getChars(4))) {
// not a texture file
return false;
}
// type
type = load.getInt();
// width
width = load.getInt();
// height
height = load.getInt();
// number of mips
int loadmips = load.getInt();
int mips = loadmips;
if(mips<1)
mips = 1;
mipMapSizes = new int[mips];
int size = 0;
if( loadmips == 0) {
size = load.remaining();
mipMapSizes[0] = size;
} else {
// load all the mips sizes
for(int i=0; i<loadmips; i++) {
mipMapSizes[i] = load.getInt();
size += mipMapSizes[i];
}
}
if( size != load.remaining()) {
// the file lenght does not match the total mipmap size
return false;
}
// create the data buffer
data = BufferUtils.createByteBuffer(size);
data.clear();
// put the remaining bytes into the buffer
data.put(load.getData());
return true;
}
Just for completeness, the DataBuffer class used for manipulating ByteBuffers:
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
*
* @author vear
*/
public class DataBuffer {
public static DataBuffer allocate(int bytes) {
ByteBuffer data = ByteBuffer.allocate(bytes);
DataBuffer db = new DataBuffer();
db.setData(data);
return db;
}
public static DataBuffer allocateDirect(int bytes) {
ByteBuffer data = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder());
DataBuffer db = new DataBuffer();
db.setData(data);
return db;
}
private ByteBuffer data;
public void clear() {
data.clear();
}
public int getInt() {
return data.getInt();
}
public int remaining() {
return data.remaining();
}
public void rewind() {
data.rewind();
}
public void put(int type) {
data.putInt(type);
}
public void put(ByteBuffer toput) {
data.put(toput);
}
public void put(String str) {
data.put(str.getBytes());
}
public String getChars(int len) {
byte b[]=new byte[len];
data.get(b, data.position(), len);
String hdr=new String(b);
return hdr;
}
public void setData(ByteBuffer data) {
this.data = data;
}
public ByteBuffer getData() {
return data;
}
The X-Shift project still needs more people:
http://www.jmonkeyengine.com/jmeforum/index.php?topic=6964.0