OpenGL ES SDK for Android ARM Developer Center
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
ETC2 Texture

Demonstration of ETC2 texture compression support in OpenGL ES 3.0.

Introduction

It is assumed that you have read and understood all of the mechanisms described in Asset Loading,Simple Triangle and Texture Cube.

Overview

ETC2TextureDemo_android.png
The application cycles through all of the texture formats supported by OpenGL ES 3.0.

Compressed textures are loaded and displayed on the screen. The internal format of each texture is displayed at the bottom of the screen. The application cycles through all of the texture formats supported by OpenGL ES 3.0.

Formats:

  1. GL_COMPRESSED_R11_EAC: 11 bits for a single channel. Useful for single channel data where higher than 8 bit precision is needed. For example, heightmaps.
  2. GL_COMPRESSED_SIGNED_R11_EAC: Signed version of GL_COMPRESSED_SIGNED_R11_EAC, useful when signed data is needed.
  3. GL_COMPRESSED_RG11_EAC: 11 bits for two channels. Useful for two channel data where higher than 8 bit precision is needed. For example, normalised bump maps, the third component can be reconstructed from the other two components.
  4. GL_COMPRESSED_SIGNED_RG11_EAC: Signed version of GL_COMPRESSED_RG11_EAC, useful when signed data is needed.
  5. GL_COMPRESSED_RGB8_ETC2: 8 bits for three channels. Useful for normal textures without alpha values.
  6. GL_COMPRESSED_SRGB8_ETC2: sRGB version of GL_COMPRESSED_RGB8_ETC2.
  7. GL_COMPRESSED_RGBA8_ETC2_EAC: 8 bits for four channels. Useful for normal textures with varying alpha values.
  8. GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: sRGB version of GL_COMPRESSED_RGBA8_ETC2_EAC.
  9. GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: 8 bits for three channels and a 1 bit alpha channel. Useful for normal textures with binary alpha values.
  10. GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: sRGB version of GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2.

Generating Compressed Images

One of the possible ways of generating ETC2 images is to use the ARM Mali Texture Compression Tool which you can find here: http://malideveloper.arm.com. Once you have installed the tool, you can load any texture (stored in jpg, png or any other supported file formats). Then try to compress the image using the ETC2 compressor by selecting the compression format you are interested in. The result will be stored in a PKM file.

ETC2TextureDemo_TctCompression.png
Generating compressed files using the ARM Mali Texture Compression Tool.

When you generate compressed textures, you can use them in the following steps.

Render Compressed Textures on a Screen

In this section we will describe how to render a compressed texture onto the screen. For each ETC2 compression format supported by OpenGL ES 3.0, we will have a separate texture object. In the steps described below, we will however focus on dealing with a single texture object, as the same steps should be repeated for all the texture formats we want to display.

Generate Texture Object

We need to start by generating a texture object ID.

GL_CHECK(glGenTextures(1, &imageArray[textureIndex].textureId));

Once we get one, we need to bind it to the GL_TEXTURE_2D target.

GL_CHECK(glBindTexture(GL_TEXTURE_2D, imageArray[textureIndex].textureId));

It is important to set the texture object's parameters, so that the texture will be properly displayed on the screen.

/* Set parameters for a texture. */
GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));

Now, we fill the texture object with data.

/* Call CompressedTexImage2D() function which specifies texture with compressed image. */
GL_CHECK(glCompressedTexImage2D(GL_TEXTURE_2D,
0,
imageWidth,
imageHeight,
0,
imageData));

If the internalformat argument value is considered, you should use one of the ETC2 internalformats supported by OpenGL ES 3.0, which are listed below (which of course correspond to the internalformat of the compressed texture we are using here).

Compressed Internal Format Image Size
GL_COMPRESSED_R11_EAC ceil(width/4) * ceil(height/4) * 8
GL_COMPRESSED_SIGNED_R11_EAC ceil(width/4) * ceil(height/4) * 8
GL_COMPRESSED_RG11_EAC ceil(width/4) * ceil(height/4) * 16
GL_COMPRESSED_SIGNED_RG11_EAC ceil(width/4) * ceil(height/4) * 16
GL_COMPRESSED_RGB8_ETC2 ceil(width/4) * ceil(height/4) * 8
GL_COMPRESSED_SRGB8_ETC2 ceil(width/4) * ceil(height/4) * 8
GL_COMPRESSED_RGBA8_ETC2_EAC ceil(width/4) * ceil(height/4) * 16
GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC ceil(width/4) * ceil(height/4) * 16
GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 ceil(width/4) * ceil(height/4) * 8
GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2ceil(width/4) * ceil(height/4) * 8

At this point there is one thing that is unclear: what values should be used for imageWidth, imageHeight, imageSize, imageData in glCompressedTexImage2D() call? Those values should correspond to the data retrieved from PKM file, as shown below.

Texture::loadPKMData(fileName, &etcHeader, &imageData);
int imageHeight = etcHeader.getHeight();
int imageWidth = etcHeader.getWidth();
GLsizei imageSize = etcHeader.getSize(internalformat);

Load PKM image data

The very first thing you should do is to set the pixel storage mode so that the proper alignment will be used whilst reading the texture images.

/* Set OpenGL to use right alignment when reading texture images. */
GL_CHECK(glPixelStorei(GL_UNPACK_ALIGNMENT, 1));

Then, you should open and read the PKM file as implemented in the functions presented below.

void Texture::loadPKMData(const char *filename, ETCHeader* etcHeader, unsigned char **textureData)
{
/* PKM file consists of a header with information about image (stored in 16 first bits) and image data. */
const int sizeOfETCHeader = 16;
unsigned char* tempTextureData = NULL;
loadData(filename, &tempTextureData);
ASSERT(textureData != NULL, "textureData is a NULL pointer.");
ASSERT(etcHeader != NULL, "etcHeader is a NULL pointer.");
ASSERT(tempTextureData != NULL, "Could not load data from PKM file.");
ETCHeader tempEtcHeader(tempTextureData);
*etcHeader = tempEtcHeader;
*textureData = tempTextureData + sizeOfETCHeader;
}
void Texture::loadData(const char *filename, unsigned char **textureData)
{
FILE *file = fopen(filename, "rb");
if(file == NULL)
{
LOGE("Failed to open '%s'\n", filename);
exit(1);
}
fseek(file, 0, SEEK_END);
unsigned int length = ftell(file);
unsigned char *loadedTexture = (unsigned char *)calloc(length, sizeof(unsigned char));
ASSERT(loadedTexture != NULL, "Could not allocate memory to store PKM file data.")
fseek(file, 0, SEEK_SET);
size_t read = fread(loadedTexture, sizeof(unsigned char), length, file);
ASSERT(read == length, "Failed to read PKM file.");
fclose(file);
*textureData = loadedTexture;
}

Once you get the result, you can use the retrieved data in the glCompressedTexImage2D() call as already mentioned in the previous section (Generate Texture Object).

Render texture on a screen

In OpenGL ES, the basic geometry rendering technique is to render triangles. In our case, we want to render a simple quad onto which there will be a texture image applied.

The first step is to generate vertex coordinates of the triangles that make up the quad (please note that we are interested in a quad in XY space).

Please look at the image below. The schema show coordinates we are using in the application.

ETC2TextureDemo_Coordinates.png
Quad coordinates (blue) and corresponding texture UVs (red).

This is defined as follows.

Coordinates of a triangles that are used to render a quad.

float vertexData[] = {-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f};

Corresponding texture UVs.

float textureCoordinatesData[] = {0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
0.0f, 0.0f,
1.0f, 1.0f,
1.0f, 0.0f};

We will need array buffer objects to store the coordinates described above.

/* Generate buffers. */
GL_CHECK(glGenBuffers(2,
/* Fill buffer object with vertex data. */
GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER,
GL_CHECK(glBufferData(GL_ARRAY_BUFFER,
sizeof(vertexData),
vertexData,
GL_STATIC_DRAW));
/* Fill buffer object with texture coordinates data. */
GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER,
GL_CHECK(glBufferData(GL_ARRAY_BUFFER,
sizeof(textureCoordinatesData),
textureCoordinatesData,
GL_STATIC_DRAW));

There is no OpenGL ES 3.0 rendering without program objects, so we should now focus on that. First of all, we need to:

  1. create program object ID,
    programId = GL_CHECK(glCreateProgram());
  2. create shader objects' IDs (should be called twice for shaderType equal to GL_FRAGMENT_SHADER and GL_VERTEX_SHADER),
    *shaderObjectIdPtr = GL_CHECK(glCreateShader(shaderType));
  3. set shader source,
    strings[0] = loadShader(filename);
    GL_CHECK(glShaderSource(*shaderObjectIdPtr, 1, strings, NULL));
  4. compile shader (it is always a good idea to check, whether the compilation succeeded: GL_COMPILE_STATUS set to GL_TRUE,
    GL_CHECK(glCompileShader(*shaderObjectIdPtr));
    GL_CHECK(glGetShaderiv(*shaderObjectIdPtr, GL_COMPILE_STATUS, &compileStatus));
  5. attach shaders to program object,
    GL_CHECK(glAttachShader(programId, vertexShaderId));
    GL_CHECK(glAttachShader(programId, fragmentShaderId));
  6. link program object,
    GL_CHECK(glLinkProgram(programId));
  7. use program.
    GL_CHECK(glUseProgram(programId));
    The shaders we are using are rather simple and self-explanatory

Vertex shader code:

in vec4 attributePosition;
in vec2 attributeTextureCoordinate;
out vec2 varyingTextureCoordinate;
void main()
{
varyingTextureCoordinate = attributeTextureCoordinate;
gl_Position = modelViewMatrix * attributePosition;
}

Fragment shader code

precision mediump float;
uniform sampler2D uniformTexture;
in vec2 varyingTextureCoordinate;
void main()
{
colour = texture(uniformTexture, varyingTextureCoordinate);
}

The next step is to set values for the attributes and uniforms we are using in the shaders. To do that, we need to retrieve their locations. In the API, we are calling glGetUniformLocation() with the ID of a program and name of the uniform used in the shaders given as arguments. The analogous situation is to call glGetAttribLocation(), but this time we are using the attribute name instead.

/* Get attributes and uniforms locations from shaders attached to the program. */
modelViewMatrixLocation = GL_CHECK(glGetUniformLocation(programId, "modelViewMatrix"));
positionLocation = GL_CHECK(glGetAttribLocation (programId, "attributePosition"));
textureCoordinateLocation = GL_CHECK(glGetAttribLocation (programId, "attributeTextureCoordinate"));
textureLocation = GL_CHECK(glGetUniformLocation(programId, "uniformTexture"));

It is always a good idea to make sure that the uniform and attributes were found (which means that they are declared and used by the program object). If the returned value is equal to -1, then an attribute/uniform is considered inactive.

ASSERT(modelViewMatrixLocation != -1, "Could not retrieve uniform location: modelViewMatrix.");
ASSERT(positionLocation != -1, "Could not retrieve attribute location: attributePosition.");
ASSERT(textureCoordinateLocation != -1, "Could not retrieve attribute location: attributeTextureCoordinate.");
ASSERT(textureLocation != -1, "Could not retrieve uniform location: uniformTexture.");

Once we are sure that retrieved locations are valid, we can set values for the uniforms and attributes.

By calling

GL_CHECK(glUniform1i (textureLocation,
0));

we are telling OpenGL ES to use a texture object that is currently bound to the GL_TEXTURE_2D target at the GL_TEXTURE0 texture unit (if you are using GL_TEXTURE1 as the texture unit, the value for the uniform should equal 1) as an input to the program object.

We wanted the application to cycle through all of the texture formats supported by OpenGL ES 3.0, which is why we will be re-binding the textures many times. This is done as shown below.

/* Draw texture-mapped quad. */
GL_CHECK(glActiveTexture(GL_TEXTURE0));
GL_CHECK(glBindTexture (GL_TEXTURE_2D,

How do we set values for attributes? The best way is to use Vertex Attrib Arrays. Do you remember the buffer objects we have created and filled with quad coordinates and texture UV data? We will use them now.

GL_CHECK(glBindBuffer (GL_ARRAY_BUFFER,
GL_CHECK(glVertexAttribPointer (positionLocation,
3,
GL_FLOAT,
GL_FALSE,
0,
0));
GL_CHECK(glEnableVertexAttribArray(positionLocation));
GL_CHECK(glBindBuffer (GL_ARRAY_BUFFER,
GL_CHECK(glVertexAttribPointer (textureCoordinateLocation,
2,
GL_FLOAT,
GL_FALSE,
0,
0));
GL_CHECK(glEnableVertexAttribArray(textureCoordinateLocation));

The last thing to do is to issue the draw call.

GL_CHECK(glDrawArrays(GL_TRIANGLES, 0, 6));