Alpha Textures

Last Updated: Aug 9th, 2012
In the Bitmap Font tutorial, we had an RGBA texture but we only cared about how bright each pixel is to use for alpha transparency. To save texture space, we'll add the ability to render alpha textures that only have an alpha component. Using 8bit pixels will save 75% of the space used by 32bit pixels.From LTexture.h
//Texture name
GLuint mTextureID;
//Current pixels
GLuint* mPixels32;
GLubyte* mPixels8;
//Pixel format
GLuint mPixelFormat;
//Texture dimensions
GLuint mTextureWidth;
GLuint mTextureHeight;
//Unpadded image dimensions
GLuint mImageWidth;
GLuint mImageHeight;
//VBO IDs
GLuint mVBOID;
GLuint mIBOID;
};
In the LTexture class, "mPixels" has been changes to "mPixels32" to distinguish it from "mPixels8" which is a pointer to 8bit pixels. Notice that "mPixels8" is a GLubyte pointer,
which stands for GL unsigned byte.
We also have "mPixelFormat" which keeps track of what kind of pixel data we're using.
We also have "mPixelFormat" which keeps track of what kind of pixel data we're using.
From LTexture.h
bool loadTextureFromFile32( std::string path );
/*
Pre Condition:
-A valid OpenGL context
-Initialized DevIL
Post Condition:
-Creates RGBA texture from the given file
-Pads image to have power-of-two dimensions
-Reports error to console if texture could not be created
Side Effects:
-Binds a NULL texture
*/
bool loadPixelsFromFile32( std::string path );
/*
Pre Condition:
-Initialized DevIL
Post Condition:
-Loads member 32bit pixels from the given file
-Pads image to have power-of-two dimensions
-Reports error to console if pixels could not be loaded
Side Effects:
-None
*/
bool loadTextureFromFileWithColorKey32( std::string path, GLubyte r, GLubyte g, GLubyte b, GLubyte a = 000 );
/*
Pre Condition:
-A valid OpenGL context
-Initialized DevIL
Post Condition:
-Creates RGBA texture from the given file
-Pads image to have power-of-two dimensions
-Sets given RGBA value to RFFGFFBFFA00 in pixel data
-If A = 0, only RGB components are compared
-Reports error to console if texture could not be created
Side Effects:
-Binds a NULL texture
*/
Our texture loading functions have been renamed to specify that they load 32bit pixel data.
From LTexture.h
bool loadPixelsFromFile8( std::string path );
/*
Pre Condition:
-Initialized DevIL
Post Condition:
-Loads member 8bit pixels from the given file
-Pads image to have power-of-two dimensions
-Reports error to console if pixels could not be loaded
Side Effects:
-None
*/
bool loadTextureFromPixels8();
/*
Pre Condition:
-A valid OpenGL context
-Valid member pixels
Post Condition:
-Creates alpha texture from the 8bit member pixels
-Deletes member pixels on success
-Reports error to console if texture could not be created
Side Effects:
-Binds a NULL texture
*/
We now have 8bit versions of pixel loading and texture generating functions.
From LTexture.h
GLubyte* getPixelData8();
/*
Pre Condition:
-Available 8bit member pixels
Post Condition:
-Returns 8bit member pixels
Side Effects:
-None
*/
We also have an 8bit pixel data accessor.
From LTexture.h
GLubyte getPixel8( GLuint x, GLuint y );
/*
Pre Condition:
-Available 8bit member pixels
Post Condition:
-Returns pixel at given position
-Function will segfault if the texture is not locked.
Side Effects:
-None
*/
void setPixel8( GLuint x, GLuint y, GLubyte pixel );
/*
Pre Condition:
-Available 8bit member pixels
Post Condition:
-Sets pixel at given position
-Function will segfault if the texture is not locked.
Side Effects:
-None
*/
And of course we have our 8 bit pixel manipulators.
From LTexture.cpp
LTexture::LTexture()
{
//Initialize texture ID and pixels
mTextureID = 0;
mPixels32 = NULL;
mPixels8 = NULL;
mPixelFormat = NULL;
//Initialize image dimensions
mImageWidth = 0;
mImageHeight = 0;
//Initialize texture dimensions
mTextureWidth = 0;
mTextureHeight = 0;
//Initialize VBO
mVBOID = 0;
mIBOID = 0;
}
As always, never forget to initialize your pointers.
From lTexture.cpp
bool LTexture::loadTextureFromPixels32( GLuint* pixels, GLuint imgWidth, GLuint imgHeight, GLuint texWidth, GLuint texHeight )
{
//Free texture data if needed
freeTexture();
//Get image dimensions
mImageWidth = imgWidth;
mImageHeight = imgHeight;
mTextureWidth = texWidth;
mTextureHeight = texHeight;
//Generate texture ID
glGenTextures( 1, &mTextureID );
//Bind texture ID
glBindTexture( GL_TEXTURE_2D, mTextureID );
//Generate texture
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, mTextureWidth, mTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels );
//Set texture parameters
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, DEFAULT_TEXTURE_WRAP );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, DEFAULT_TEXTURE_WRAP );
//Unbind texture
glBindTexture( GL_TEXTURE_2D, NULL );
//Check for error
GLenum error = glGetError();
if( error != GL_NO_ERROR )
{
printf( "Error loading texture from %p pixels! %s\n", pixels, gluErrorString( error ) );
return false;
}
//Generate VBO
initVBO();
//Set pixel format
mPixelFormat = GL_RGBA;
return true;
}
All of our 32bit pixel/texture loading functions (which we're not going to go through individually) now also specify what the pixel format for the data is.
From LTexture.cpp
bool LTexture::loadPixelsFromFile8( std::string path )
{
//Free texture data if needed
freeTexture();
//Texture loading success
bool pixelsLoaded = false;
//Generate and set current image ID
ILuint imgID = 0;
ilGenImages( 1, &imgID );
ilBindImage( imgID );
//Load image
ILboolean success = ilLoadImage( path.c_str() );
//Image loaded successfully
if( success == IL_TRUE )
{
//Convert image to grey scale
success = ilConvertImage( IL_LUMINANCE, IL_UNSIGNED_BYTE );
if( success == IL_TRUE )
{
//Initialize dimensions
GLuint imgWidth = (GLuint)ilGetInteger( IL_IMAGE_WIDTH );
GLuint imgHeight = (GLuint)ilGetInteger( IL_IMAGE_HEIGHT );
//Calculate required texture dimensions
GLuint texWidth = powerOfTwo( imgWidth );
GLuint texHeight = powerOfTwo( imgHeight );
//Texture is the wrong size
if( imgWidth != texWidth || imgHeight != texHeight )
{
//Place image at upper left
iluImageParameter( ILU_PLACEMENT, ILU_UPPER_LEFT );
//Resize image
iluEnlargeCanvas( (int)texWidth, (int)texHeight, 1 );
}
//Allocate memory for texture data
GLuint size = texWidth * texHeight;
mPixels8 = new GLubyte[ size ];
//Get image dimensions
mImageWidth = imgWidth;
mImageHeight = imgHeight;
mTextureWidth = texWidth;
mTextureHeight = texHeight;
//Copy pixels
memcpy( mPixels8, ilGetData(), size );
pixelsLoaded = true;
}
//Delete file from memory
ilDeleteImages( 1, &imgID );
//Set pixel format
mPixelFormat = GL_ALPHA;
}
//Report error
if( !pixelsLoaded )
{
printf( "Unable to load %s\m", path.c_str() );
}
return pixelsLoaded;
}
The loadPixelsFromFile8() function is largely the same as our old loadPixelsFromFile32() function with a few key differences. It allocates an array of GLubytes (remember we're
dealing with 8bit data here) to the "mPixel8" pointer and sets the pixel format as "GL_ALPHA". When it copies the pixels with memcpy(), it's only one byte per pixel so we don't
multiply the size by four like with did with RGBA data.
What also changes is how DevIL loads the pixel data. Before ilConvertImage() converted the pixel to RGBA. Now it converts the pixels to luminance. Luminance pixels have a single byte that says how bright they are. Like in the original Bitmap Font tutorial, we're going to use the brightness of the pixels to smooth blend the text.
What also changes is how DevIL loads the pixel data. Before ilConvertImage() converted the pixel to RGBA. Now it converts the pixels to luminance. Luminance pixels have a single byte that says how bright they are. Like in the original Bitmap Font tutorial, we're going to use the brightness of the pixels to smooth blend the text.
From LTexture.cpp
bool LTexture::loadTextureFromPixels8()
{
//Loading flag
bool success = true;
//There is loaded pixels
if( mTextureID == 0 && mPixels8 != NULL )
{
//Generate texture ID
glGenTextures( 1, &mTextureID );
//Bind texture ID
glBindTexture( GL_TEXTURE_2D, mTextureID );
//Generate texture
glTexImage2D( GL_TEXTURE_2D, 0, GL_ALPHA, mTextureWidth, mTextureHeight, 0, GL_ALPHA, GL_UNSIGNED_BYTE, mPixels8 );
//Set texture parameters
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, DEFAULT_TEXTURE_WRAP );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, DEFAULT_TEXTURE_WRAP );
//Unbind texture
glBindTexture( GL_TEXTURE_2D, NULL );
//Check for error
GLenum error = glGetError();
if( error != GL_NO_ERROR )
{
printf( "Error loading texture from %p pixels! %s\n", mPixels8, gluErrorString( error ) );
success = false;
}
else
{
//Release pixels
delete[] mPixels8;
mPixels8 = NULL;
//Generate VBO
initVBO();
//Set pixel format
mPixelFormat = GL_ALPHA;
}
}
//Error
else
{
printf( "Cannot load texture from current pixels! " );
//Texture already exists
if( mTextureID != 0 )
{
printf( "A texture is already loaded!\n" );
}
//No pixel loaded
else if( mPixels8 == NULL )
{
printf( "No pixels to create texture from!\n" );
}
}
return success;
}
When creating an alpha texture, you have to set the pixel format as "GL_ALPHA" instead of "GL_RGBA" we sending pixels with glTexImage2D().
From LTexture.cpp
void LTexture::freeTexture()
{
//Delete texture
if( mTextureID != 0 )
{
glDeleteTextures( 1, &mTextureID );
mTextureID = 0;
}
//Delete 32bit pixels
if( mPixels32 != NULL )
{
delete[] mPixels32;
mPixels32 = NULL;
}
//Delete 8bit pixels
if( mPixels8 != NULL )
{
delete[] mPixels8;
mPixels8 = NULL;
}
mImageWidth = 0;
mImageHeight = 0;
mTextureWidth = 0;
mTextureHeight = 0;
//Set pixel format
mPixelFormat = NULL;
}
When freeing textures, we have to get rid of 8bit pixel data too.
From LTexture.cpp
bool LTexture::lock()
{
//If texture is not locked and a texture exists
if( mPixels32 == NULL && mPixels8 == NULL && mTextureID != 0 )
{
//Allocate memory for texture data
GLuint size = mTextureWidth * mTextureHeight;
if( mPixelFormat == GL_RGBA )
{
mPixels32 = new GLuint[ size ];
}
else if( mPixelFormat == GL_ALPHA )
{
mPixels8 = new GLubyte[ size ];
}
//Set current texture
glBindTexture( GL_TEXTURE_2D, mTextureID );
//Get pixels
glGetTexImage( GL_TEXTURE_2D, 0, mPixelFormat, GL_UNSIGNED_BYTE, mPixels32 );
//Unbind texture
glBindTexture( GL_TEXTURE_2D, NULL );
return true;
}
return false;
}
When locking the texture for updating we make sure that the texture isn't already locked. Then we allocate the proper pixel memory and get the pixel data from the texture. This
time glGetTexImage() takes in the pixel format to get the proper pixels.
From LTexture.cpp
bool LTexture::unlock()
{
//If texture is locked and a texture exists
if( ( mPixels32 != NULL || mPixels8 != NULL ) && mTextureID != 0 )
{
//Set current texture
glBindTexture( GL_TEXTURE_2D, mTextureID );
//Update texture
void* pixels = ( mPixelFormat == GL_RGBA ) ? (void*)mPixels32 : (void*)mPixels8;
glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, mTextureWidth, mTextureHeight, mPixelFormat, GL_UNSIGNED_BYTE, pixels );
//Delete pixels
if( mPixels32 != NULL )
{
delete[] mPixels32;
mPixels32 = NULL;
}
if( mPixels8 != NULL )
{
delete[] mPixels8;
mPixels8 = NULL;
}
//Unbind texture
glBindTexture( GL_TEXTURE_2D, NULL );
return true;
}
return false;
}
When updating the texture, we want to make sure have pixels to update with and a texture to update. Then we bind the texture and select which pixels to send with the ternary
operator.
For those of you unfimiliar with the ternary operator, it's a fancy way to stick an if/else statement in one line. In a nutshell, it works like this:
(condition) ? return this if true : return this if false.
So if the pixel format is RGBA, get the 32bit pixels and if it's not get the 8bit pixels.
After updating the pixels with glTexSubImage2D(), we deallocate the pixel data and unbind the texture.
For those of you unfimiliar with the ternary operator, it's a fancy way to stick an if/else statement in one line. In a nutshell, it works like this:
(condition) ? return this if true : return this if false.
So if the pixel format is RGBA, get the 32bit pixels and if it's not get the 8bit pixels.
After updating the pixels with glTexSubImage2D(), we deallocate the pixel data and unbind the texture.
From LTexture.cpp
GLuint* LTexture::getPixelData32()
{
return mPixels32;
}
GLubyte* LTexture::getPixelData8()
{
return mPixels8;
}
GLuint LTexture::getPixel32( GLuint x, GLuint y )
{
return mPixels32[ y * mTextureWidth + x ];
}
void LTexture::setPixel32( GLuint x, GLuint y, GLuint pixel )
{
mPixels32[ y * mTextureWidth + x ] = pixel;
}
GLubyte LTexture::getPixel8( GLuint x, GLuint y )
{
return mPixels8[ y * mTextureWidth + x ];
}
void LTexture::setPixel8( GLuint x, GLuint y, GLubyte pixel )
{
mPixels8[ y * mTextureWidth + x ] = pixel;
}
Our pixel manipulating functions work pretty much the same. The only difference is which pointer they use.
From LFont.cpp
bool LFont::loadBitmap( std::string path )
{
//Loading flag
bool success = true;
//Background pixel
const GLubyte BLACK_PIXEL = 0x00;
//Get rid of the font if it exists
freeFont();
//Image pixels loaded
if( loadPixelsFromFile8( path ) )
{
//Get cell dimensions
GLfloat cellW = imageWidth() / 16.f;
GLfloat cellH = imageHeight() / 16.f;
//Get letter top and bottom
GLuint top = cellH;
GLuint bottom = 0;
GLuint aBottom = 0;
//Current pixel coordinates
int pX = 0;
int pY = 0;
//Base cell offsets
int bX = 0;
int bY = 0;
//Begin parsing bitmap font
GLuint currentChar = 0;
LFRect nextClip = { 0.f, 0.f, cellW, cellH };
//Go through cell rows
for( unsigned int rows = 0; rows < 16; ++rows )
{
//Go through each cell column in the row
for( unsigned int cols = 0; cols < 16; ++cols )
{
//Begin cell parsing
//Set base offsets
bX = cellW * cols;
bY = cellH * rows;
//Initialize clip
nextClip.x = cellW * cols;
nextClip.y = cellH * rows;
nextClip.w = cellW;
nextClip.h = cellH;
With the LTexture class updated to handle 8bit textures, loadBitmap() can use this to save a lot memory when loading bitmap fonts.
The top of the function looks pretty much the same with a few difference. With 8bit luminence pixels, color black is just a 0 byte. Obviously, we also change the pixel loading function to loadPixelsFromFile8() to load the 8bit pixels.
The top of the function looks pretty much the same with a few difference. With 8bit luminence pixels, color black is just a 0 byte. Obviously, we also change the pixel loading function to loadPixelsFromFile8() to load the 8bit pixels.
From LFont.cpp
//Find left side of character
for( int pCol = 0; pCol < cellW; ++pCol )
{
for( int pRow = 0; pRow < cellH; ++pRow )
{
//Set pixel offset
pX = bX + pCol;
pY = bY + pRow;
//Non-background pixel found
if( getPixel8( pX, pY ) != BLACK_PIXEL )
{
//Set sprite's x offset
nextClip.x = pX;
//Break the loops
pCol = cellW;
pRow = cellH;
}
}
}
//Right side
for( int pCol_w = cellW - 1; pCol_w >= 0; pCol_w-- )
{
for( int pRow_w = 0; pRow_w < cellH; pRow_w++ )
{
//Set pixel offset
pX = bX + pCol_w;
pY = bY + pRow_w;
//Non-background pixel found
if( getPixel8( pX, pY ) != BLACK_PIXEL )
{
//Set sprite's width
nextClip.w = ( pX - nextClip.x ) + 1;
//Break the loops
pCol_w = -1;
pRow_w = cellH;
}
}
}
//Find Top
for( int pRow = 0; pRow < cellH; ++pRow )
{
for( int pCol = 0; pCol < cellW; ++pCol )
{
//Set pixel offset
pX = bX + pCol;
pY = bY + pRow;
//Non-background pixel found
if( getPixel8( pX, pY ) != BLACK_PIXEL )
{
//New Top Found
if( pRow < top )
{
top = pRow;
}
//Break the loops
pCol = cellW;
pRow = cellH;
}
}
}
//Find Bottom
for( int pRow_b = cellH - 1; pRow_b >= 0; --pRow_b )
{
for( int pCol_b = 0; pCol_b < cellW; ++pCol_b )
{
//Set pixel offset
pX = bX + pCol_b;
pY = bY + pRow_b;
//Non-background pixel found
if( getPixel8( pX, pY ) != BLACK_PIXEL )
{
//Set BaseLine
if( currentChar == 'A' )
{
aBottom = pRow_b;
}
//New bottom Found
if( pRow_b > bottom )
{
bottom = pRow_b;
}
//Break the loops
pCol_b = cellW;
pRow_b = -1;
}
}
}
//Go to the next character
mClips.push_back( nextClip );
++currentChar;
}
}
Parsing each of the sprite cells works pretty much the same, only now we're getting and comparing 8bit pixels.
From LFont.cpp
//Set Top
for( int t = 0; t < 256; ++t )
{
mClips[ t ].y += top;
mClips[ t ].h -= top;
}
//Create texture from parsed pixels
if( loadTextureFromPixels8() )
{
//Build vertex buffer from sprite sheet data
if( !generateDataBuffer( LSPRITE_ORIGIN_TOP_LEFT ) )
{
printf( "Unable to create vertex buffer for bitmap font!\n" );
success = false;
}
}
else
{
printf( "Unable to create texture from bitmap font pixels!\n" );
success = false;
}
//Set texture wrap
glBindTexture( GL_TEXTURE_2D, getTextureID() );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER );
//Set spacing variables
mSpace = cellW / 2;
mNewLine = aBottom - top;
mLineHeight = bottom - top;
}
else
{
printf( "Could not load bitmap font image: %s!\n", path.c_str() );
success = false;
}
return success;
}
After parsing all of the cells, the rest of the code should look pretty familiar. The tops of the sprites are set, the texture is loaded (this time in 8bit), generate the VBO data
the same as before, set font texure wrap, and set the spacing variables.
This time we skipped the blending of the pixels since we don't need to blend pixels that are already in alpha format.
This time we skipped the blending of the pixels since we don't need to blend pixels that are already in alpha format.
From LFont.cpp
void LFont::renderText( GLfloat x, GLfloat y, std::string text )
{
//If there is a texture to render from
if( getTextureID() != 0 )
{
//Draw positions
GLfloat dX = x;
GLfloat dY = y;
//Move to draw position
glTranslatef( x, y, 0.f );
//Set texture
glBindTexture( GL_TEXTURE_2D, getTextureID() );
//Enable vertex and texture coordinate arrays
glEnableClientState( GL_VERTEX_ARRAY );
glEnableClientState( GL_TEXTURE_COORD_ARRAY );
//Bind vertex data
glBindBuffer( GL_ARRAY_BUFFER, mVertexDataBuffer );
//Set texture coordinate data
glTexCoordPointer( 2, GL_FLOAT, sizeof(LVertexData2D), (GLvoid*) offsetof( LVertexData2D, texCoord ) );
//Set vertex data
glVertexPointer( 2, GL_FLOAT, sizeof(LVertexData2D), (GLvoid*) offsetof( LVertexData2D, position ) );
//Go through string
for( int i = 0; i < text.length(); ++i )
{
//Space
if( text[ i ] == ' ' )
{
glTranslatef( mSpace, 0.f, 0.f );
dX += mSpace;
}
//Newline
else if( text[ i ] == '\n' )
{
glTranslatef( x - dX, mNewLine, 0.f );
dY += mNewLine;
dX += x - dX;
}
//Character
else
{
//Get ASCII
GLuint ascii = (unsigned char)text[ i ];
//Draw quad using vertex data and index data
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mIndexBuffers[ ascii ] );
glDrawElements( GL_QUADS, 4, GL_UNSIGNED_INT, NULL );
//Move over
glTranslatef( mClips[ ascii ].w, 0.f, 0.f );
dX += mClips[ ascii ].w;
}
}
//Disable vertex and texture coordinate arrays
glDisableClientState( GL_TEXTURE_COORD_ARRAY );
glDisableClientState( GL_VERTEX_ARRAY );
}
}
As you can see, the rendering of the text works exactly the same. A different pixel format just means the color is handled a little differently.
From LUtil.cpp
bool loadMedia()
{
//Load Font
if( !gFont.loadBitmap( "21_alpha_textures/lazy_font.png" ) )
{
printf( "Unable to load bitmap font!\n" );
return false;
}
return true;
}
void update()
{
}
void render()
{
//Clear color buffer
glClear( GL_COLOR_BUFFER_BIT );
glLoadIdentity();
//Render green text
glColor3f( 0.f, 1.f, 0.f );
gFont.renderText( 0.f, 0.f, "The quick brown fox jumps\nover the lazy dog, again!" );
//Update screen
glutSwapBuffers();
}
From the outside, the bitmap font seems to work exactly the same, even though it uses much less texture memory than it used to.
You may be wondering "If all we have is the alpha value, what are the RGB values?". When using an alpha texture, the RGB values are set to white. This rendering of text will produce green text much in the same way the bitmap font produced red text in the previous tutorial.
You may be wondering "If all we have is the alpha value, what are the RGB values?". When using an alpha texture, the RGB values are set to white. This rendering of text will produce green text much in the same way the bitmap font produced red text in the previous tutorial.