Lazy Foo' Productions

SDL Forums external link SDL Tutorials Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Alpha Textures

Alpha Textures screenshot

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.
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.
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.
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.
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.
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.