ETC1 Android compressed textures since 2.2

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, 2
    m_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]
2 Likes

That’s nice thank you!!

@Normen, this could be done at compile time in the android build, what do you think?

@nehon said:
Normen, this could be done at compile time in the android build, what do you think?

It fits to the asset separation stuff I want to add to the SDK.. Its similar to replacing j3m or nifty xml files for android deployment.

I commited this code to SVN (after a lot of cleanup of course). You can toggle this feature via TextureUtil.ENABLE_COMPRESSION.

What I noticed is that loading time is much higher with that option enabled, and generally I did not notice a performance boost.



Can anybody else try the new jME3 version and see if they get any performance increase with the ETC compression?

I guess the only noticeable difference should be on the memory use

I noticed a performance boost of 5~10 %.

@Nehon : this kind of compression not only affect texture size but also draw speed

@kine said:
I noticed a performance boost of 5~10 %.
@Nehon : this kind of compression not only affect texture size but also draw speed

cool good news!

It would be interesting to see some metrics for both before and after the change on a range of devices - average load time and average FPS on a scene.



That will show definitely when and if it is a worthwhile change.

Hello everyone :smile:
when i use a .dds file with ETC1 compression this error occurs:

java.lang.UnsupportedOperationException : Unrecognized format: ETC1 at …

ETC1 can not be recognized ? how can i use it otherwise ?

Thanks.