Hello !
Here is a totally ugly piece of code that shows my experiments in using ETC1. ETC1 is part of Android 2.2 and gives a nice performance boost.The code convert RGB texture to RGB565 ETC1 textures on the fly. It works fine.Feel free to use it as inspiration It would be great if JME3 Android had some ETC1 loader !
So here is the code for JME devs.
Thanks
Kine
PS: to use it with mipmapping you have to deactivate glGenerateMipMaps in the Android renderer, because GLES2.0 don’t know how to auto generate mipmaps from compressed textures.
[java]
package com.jme3.renderer.android;
import android.graphics.Bitmap;
import android.opengl.ETC1;
import android.opengl.ETC1Util;
import android.opengl.ETC1Util.ETC1Texture;
import android.opengl.GLES10;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Log;
import com.jme3.asset.AndroidImageInfo;
import com.jme3.math.FastMath;
import com.jme3.texture.Image;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.microedition.khronos.opengles.GL10;
import org.lwjgl.opengl.GL20;
public class TextureUtil {
private static void buildMipmap(Bitmap bitmap, int bpp) {
int level = 0;
int height = bitmap.getHeight();
int width = bitmap.getWidth();
Log.e(“txture util”," mipmaps entering !! !! size=" + width);
while (height >= 1 || width >= 1) {
//First of all, generate the texture from our bitmap and set it to the according level
// GLUtils.texImage2D(GL10.GL_TEXTURE_2D, level, bitmap, 0);
if (bitmap.hasAlpha()) { // || bitmap.getRowBytes() !=
// bitmap.getWidth() * 3) {
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, level, bitmap, 0);
} else {
bitmap = bitmap.copy(Bitmap.Config.RGB_565, true);
bpp = 2;
int tech = 1;
if (tech == 1) {
Log.e(“txture util”," mipmaps !! size=" + width + " bpp=" + bpp);
int size = bitmap.getRowBytes() * bitmap.getHeight();
ByteBuffer bb = ByteBuffer.allocateDirect(size); // size is
// good
bb.order(ByteOrder.nativeOrder());
bitmap.copyPixelsToBuffer(bb);
bb.position(0);
ETC1Texture etc1tex;
// RGB_565 is 2 bytes per pixel
// ETC1Texture etc1tex = ETC1Util.compressTexture(bb,
// m_TexWidth, m_TexHeight, 2, 2*m_TexWidth);
int encodedImageSize = ETC1.getEncodedDataSize(
bitmap.getWidth(), bitmap.getHeight());
ByteBuffer compressedImage = ByteBuffer.allocateDirect(
encodedImageSize).order(ByteOrder.nativeOrder());
// RGB_565 is 2 bytes per pixel
ETC1.encodeImage(bb, bitmap.getWidth(), bitmap.getHeight(),
bpp, bpp * bitmap.getWidth(), compressedImage);
etc1tex = new ETC1Texture(bitmap.getWidth(),
bitmap.getHeight(), compressedImage);
// ETC1Util.loadTexture(GL10.GL_TEXTURE_2D, 0, 0,
// GL10.GL_RGB, GL10.GL_UNSIGNED_SHORT_5_6_5, etc1tex);
GLES20.glCompressedTexImage2D(GL10.GL_TEXTURE_2D, level,
ETC1.ETC1_RGB8_OES, bitmap.getWidth(),
bitmap.getHeight(), 0,
etc1tex.getData().capacity(), etc1tex.getData());
//GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
bb = null;
compressedImage = null;
etc1tex = null;
}
}
if (height == 1 || width == 1) {
break;
}
//Increase the mipmap level
level++;
height /= 2;
width /= 2;
Bitmap bitmap2 = Bitmap.createScaledBitmap(bitmap, width, height, true);
bitmap.recycle();
bitmap = bitmap2;
}
}
/**
- <code>uploadTextureBitmap</code> uploads a native android bitmap
-
@param imageInfo
/
public static void uploadTextureBitmap(final int target, Bitmap bitmap, boolean generateMips, boolean powerOf2, AndroidImageInfo imageInfo) {
if (!powerOf2) {
// Power of 2 images are not supported by this GPU.
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// If the image is not power of 2, rescale it
if (!FastMath.isPowerOfTwo(width) || !FastMath.isPowerOfTwo(height)) {
// scale to power of two, then recycle the old image.
width = FastMath.nearestPowerOfTwo(width);
height = FastMath.nearestPowerOfTwo(height);
Bitmap bitmap2 = Bitmap.createScaledBitmap(bitmap, width, height, true);
bitmap.recycle();
bitmap = bitmap2;
}
}
int bpp = 2;
Log.e("textureutil ", " image : " + imageInfo.toString());
if (imageInfo.getFormat() == com.jme3.texture.Image.Format.RGB565) {
bpp = 2;
} else {
bpp = 3;
}
if ( generateMips) {
//bitmap = bitmap.copy(Bitmap.Config.RGB_565, true);
//bpp = 2;
Log.e("textureutil"," generate mipmaps !!");
buildMipmap(bitmap,bpp);
} else {
Log.e("textutil", " bitmap.toString()" + bitmap.toString());
if (bitmap.hasAlpha() ) { // || bitmap.getRowBytes() !=
// bitmap.getWidth() * 3) {
GLUtils.texImage2D(target, 0, bitmap, 0);
return;
} else {
bitmap = bitmap.copy(Bitmap.Config.RGB_565, true);
bpp = 2;
int tech = 1;
if (tech == 1) {
int size = bitmap.getRowBytes() * bitmap.getHeight();
ByteBuffer bb = ByteBuffer.allocateDirect(size); // size is good
bb.order(ByteOrder.nativeOrder());
bitmap.copyPixelsToBuffer(bb);
bb.position(0);
ETC1Texture etc1tex;
// RGB_565 is 2 bytes per pixel
//ETC1Texture etc1tex = ETC1Util.compressTexture(bb, m_TexWidth, m_TexHeight, 2, 2m_TexWidth);
final int encodedImageSize = ETC1.getEncodedDataSize(bitmap.getWidth(), bitmap.getHeight());
ByteBuffer compressedImage = ByteBuffer.allocateDirect(encodedImageSize).order(ByteOrder.nativeOrder());
// RGB_565 is 2 bytes per pixel
ETC1.encodeImage(bb, bitmap.getWidth(), bitmap.getHeight(), bpp, bppbitmap.getWidth(), compressedImage);
etc1tex = new ETC1Texture(bitmap.getWidth(), bitmap.getHeight(), compressedImage);
//ETC1Util.loadTexture(GL10.GL_TEXTURE_2D, 0, 0, GL10.GL_RGB, GL10.GL_UNSIGNED_SHORT_5_6_5, etc1tex);
GLES20.glCompressedTexImage2D(target, 0, ETC1.ETC1_RGB8_OES, bitmap.getWidth(), bitmap.getHeight(), 0, etc1tex.getData().capacity(), etc1tex.getData());
bb = null;
compressedImage = null;
etc1tex = null;
}
else {
int size = bitmap.getRowBytes() * bitmap.getHeight();
ByteBuffer bb = ByteBuffer.allocateDirect(size); // size is
// good
bb.order(ByteOrder.nativeOrder());
bitmap.copyPixelsToBuffer(bb);
bb.position(0);
ETC1Util.ETC1Texture etc1Texture = ETC1Util
.compressTexture(bb, bitmap.getWidth(),
bitmap.getHeight(), bpp,
bpp * bitmap.getWidth());
if (bpp == 2)
ETC1Util.loadTexture(target, 0, 0, GLES10.GL_RGB,
GLES10.GL_UNSIGNED_SHORT_5_6_5, etc1Texture);
if (bpp == 3)
ETC1Util.loadTexture(target, 0, 0, GLES10.GL_RGB,
GLES20.GL_UNSIGNED_BYTE, etc1Texture);
}
/
- void glCompressedTexImage2D( GLenum target, GLint level,
- GLenum internalformat, GLsizei width, GLsizei height, GLint
- border, GLsizei imageSize, const GLvoid * data);
*/
}
//bitmap.recycle();
}
}
public static void uploadTexture(Image img,
int target,
int index,
int border,
boolean tdc,
boolean generateMips,
boolean powerOf2){
Log.e(“textueutil”,“img” + img.toString());
if (img.getEfficentData() instanceof AndroidImageInfo){
// If image was loaded from asset manager, use fast path
AndroidImageInfo imageInfo = (AndroidImageInfo) img.getEfficentData();
uploadTextureBitmap(target, imageInfo.getBitmap(), generateMips, powerOf2,imageInfo);
return;
}
// Otherwise upload image directly.
// Prefer to only use power of 2 textures here to avoid errors.
Image.Format fmt = img.getFormat();
ByteBuffer data;
if (index >= 0 || img.getData() != null && img.getData().size() > 0){
data = img.getData(index);
}else{
data = null;
}
int width = img.getWidth();
int height = img.getHeight();
int depth = img.getDepth();
boolean compress = false;
int format = -1;
int dataType = -1;
switch (fmt){
case RGBA16:
case RGB16:
case RGB10:
case Luminance16:
case Luminance16Alpha16:
case Alpha16:
case Depth32:
case Depth32F:
throw new UnsupportedOperationException(“The image format '”
- fmt + “’ is not supported by OpenGL ES 2.0 specification.”);
case Alpha8:
format = GLES20.GL_ALPHA;
dataType = GLES20.GL_UNSIGNED_BYTE;
break;
case Luminance8:
format = GLES20.GL_LUMINANCE;
dataType = GLES20.GL_UNSIGNED_BYTE;
break;
case Luminance8Alpha8:
format = GLES20.GL_LUMINANCE_ALPHA;
dataType = GLES20.GL_UNSIGNED_BYTE;
break;
case RGB565:
format = GLES20.GL_RGB;
dataType = GLES20.GL_UNSIGNED_SHORT_5_6_5;
break;
case ARGB4444:
format = GLES20.GL_RGBA4;
dataType = GLES20.GL_UNSIGNED_SHORT_4_4_4_4;
break;
case RGB5A1:
format = GLES20.GL_RGBA;
dataType = GLES20.GL_UNSIGNED_SHORT_5_5_5_1;
break;
case RGB8:
format = GLES20.GL_RGB;
dataType = GLES20.GL_UNSIGNED_BYTE;
break;
case BGR8:
format = GLES20.GL_RGB;
dataType = GLES20.GL_UNSIGNED_BYTE;
break;
case RGBA8:
format = GLES20.GL_RGBA;
dataType = GLES20.GL_UNSIGNED_BYTE;
break;
case Depth:
case Depth16:
case Depth24:
format = GLES20.GL_DEPTH_COMPONENT;
dataType = GLES20.GL_UNSIGNED_BYTE;
break;
default:
throw new UnsupportedOperationException("Unrecognized format: " + fmt);
}
if (data != null) {
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
}
int[] mipSizes = img.getMipMapSizes();
int pos = 0;
if (mipSizes == null){
if (data != null)
mipSizes = new int[]{ data.capacity() };
else
mipSizes = new int[]{ width * height * fmt.getBitsPerPixel() / 8 };
}
// XXX: might want to change that when support
// of more than paletted compressions is added…
/// NOTE: Doesn’t support mipmaps
if (compress){
data.clear();
GLES20.glCompressedTexImage2D(target,
1 - mipSizes.length,
format,
width,
height,
0,
data.capacity(),
data);
return;
}
for (int i = 0; i < mipSizes.length; i++){
int mipWidth = Math.max(1, width >> i);
int mipHeight = Math.max(1, height >> i);
// int mipDepth = Math.max(1, depth >> i);
if (data != null){
data.position(pos);
data.limit(pos + mipSizes);
}
if (compress && data != null){
GLES20.glCompressedTexImage2D(target,
i,
format,
mipWidth,
mipHeight,
0,
data.remaining(),
data);
}else{
GLES20.glTexImage2D(target,
i,
format,
mipWidth,
mipHeight,
0,
format,
dataType,
data);
}
pos += mipSizes;
}
}
}
[/java]