Lazy Foo' Productions


Texture Manipulation

Texture Manipulation screenshot

Last Updated: Jan 10th, 2024

To do graphics effects often requires pixel access. In this tutorial we'll be altering an image's pixels to white out the background.
//Texture wrapper class
class LTexture
{
    public:
        //Initializes variables
        LTexture();

        //Deallocates memory
        ~LTexture();

        //Loads image at specified path
        bool loadFromFile( std::string path );
        
        //Loads image into pixel buffer
        bool loadPixelsFromFile( std::string path );

        //Creates image from preloaded pixels
        bool loadFromPixels();

        #if defined(SDL_TTF_MAJOR_VERSION)
        //Creates image from font string
        bool loadFromRenderedText( std::string textureText, SDL_Color textColor );
        #endif

        //Deallocates texture
        void free();

        //Set color modulation
        void setColor( Uint8 red, Uint8 green, Uint8 blue );

        //Set blending
        void setBlendMode( SDL_BlendMode blending );

        //Set alpha modulation
        void setAlpha( Uint8 alpha );
        
        //Renders texture at given point
        void render( int x, int y, SDL_Rect* clip = NULL, double angle = 0.0, SDL_Point* center = NULL, SDL_RendererFlip flip = SDL_FLIP_NONE );

        //Gets image dimensions
        int getWidth();
        int getHeight();

        //Pixel accessors
        Uint32* getPixels32();
        Uint32 getPitch32();
        Uint32 mapRGBA( Uint8 r, Uint8 g, Uint8 b, Uint8 a );

    private:
        //The actual hardware texture
        SDL_Texture* mTexture;

        //Surface pixels
        SDL_Surface* mSurfacePixels;
        
        //Image dimensions
        int mWidth;
        int mHeight;
};
Here we're adding new functionality to the texture class. We want to be able to manipulate a surface's pixels before turning it into a texture, so we separate function loadPixelsFromFile() to load the pixels and then loadFromPixels() to turn the texture into pixels.

We have a function to get the raw pixels and a function to get the pitch. The pitch is basically the width of the texture in memory. On some older and mobile hardware, there are limitations of what size texture you can have. If you create a texture with a width of 100 pixels, it may get padded to 128 pixels wide (the next power of two). Using the pitch, we know how the image is in memory.

Next we have the mapRGBA() which takes in a set of red, green, blue, and alpha in 8 bits and gives us back a single 32bit pixel in the format of the internal SDL_Surface.

In terms of data members we have a pointer to the surface that holds our pixels.
bool LTexture::loadPixelsFromFile( std::string path )
{
    //Free preexisting assets
    free();

    //Load image at specified path
    SDL_Surface* loadedSurface = IMG_Load( path.c_str() );
    if( loadedSurface == NULL )
    {
        printf( "Unable to load image %s! SDL_image Error: %s\n", path.c_str(), IMG_GetError() );
    }
    else
    {
        //Convert surface to display format
        mSurfacePixels = SDL_ConvertSurfaceFormat( loadedSurface, SDL_GetWindowPixelFormat( gWindow ), 0 );
        if( mSurfacePixels == NULL )
        {
            printf( "Unable to convert loaded surface to display format! SDL Error: %s\n", SDL_GetError() );
        }
        else
        {
            //Get image dimensions
            mWidth = mSurfacePixels->w;
            mHeight = mSurfacePixels->h;
        }

        //Get rid of old loaded surface
        SDL_FreeSurface( loadedSurface );
    }

    return mSurfacePixels != NULL;
}
This function should look fairly familiar. It loads an SDL surface in a way that's similar to the way we used to in loadFromFile(), minus the texture creation.
bool LTexture::loadFromPixels()
{
    //Only load if pixels exist
    if( mSurfacePixels == NULL )
    {
        printf( "No pixels loaded!" );
    }
    else
    {
        //Color key image
        SDL_SetColorKey( mSurfacePixels, SDL_TRUE, SDL_MapRGB( mSurfacePixels->format, 0, 0xFF, 0xFF ) );

        //Create texture from surface pixels
        mTexture = SDL_CreateTextureFromSurface( gRenderer, mSurfacePixels );
        if( mTexture == NULL )
        {
            printf( "Unable to create texture from loaded pixels! SDL Error: %s\n", SDL_GetError() );
        }
        else
        {
            //Get image dimensions
            mWidth = mSurfacePixels->w;
            mHeight = mSurfacePixels->h;
        }

        //Get rid of old loaded surface
        SDL_FreeSurface( mSurfacePixels );
        mSurfacePixels = NULL;
    }

    //Return success
    return mTexture != NULL;
}
And here's the other half of our old texture loading function that actually loads the texture.
bool LTexture::loadFromFile( std::string path )
{
    //Load pixels
    if( !loadPixelsFromFile( path ) )
    {
        printf( "Failed to load pixels for %s!\n", path.c_str() );
    }
    else
    {
        //Load texture from pixels
        if( !loadFromPixels() )
        {
            printf( "Failed to texture from pixels from %s!\n", path.c_str() );
        }
    }

    //Return success
    return mTexture != NULL;
}
Finally here's the old loadFromFile() function refactored to use our separated loading functions.
Uint32* LTexture::getPixels32()
{
    Uint32* pixels = NULL;

    if( mSurfacePixels != NULL )
    {
        pixels =  static_cast( mSurfacePixels->pixels );
    }

    return pixels;
}

Uint32 LTexture::getPitch32()
{
    Uint32 pitch = 0;

    if( mSurfacePixels != NULL )
    {
        pitch = mSurfacePixels->pitch / 4;
    }

    return pitch;
}
Here are the accessors to get the pixels and pitch from the surface. You may be wondering why we are dividing the pitch by 4. Pitch is expressed in bytes and since we want the pitch in pixels and there are 32bits/4bytes per pixel, we can get the pixel per pitch by dividing the 4 bytes per pixel.
Uint32 LTexture::mapRGBA( Uint8 r, Uint8 g, Uint8 b, Uint8 a )
{
    Uint32 pixel = 0;

    if( mSurfacePixels != NULL )
    {
        pixel = SDL_MapRGBA( mSurfacePixels->format, r, g, b, a );
    }

    return pixel;
}
As you can see, our mapRGBA is just a wrapper for SDL_MapRGBA() that maps the pixel in the format of the internal surface.
bool loadMedia()
{
    //Loading success flag
    bool success = true;

    //Load Foo' texture pixel
    if( !gFooTexture.loadPixelsFromFile( "40_texture_manipulation/foo.png" ) )
    {
        printf( "Unable to load Foo' texture!\n" );
        success = false;
    }
In our media loading function we load the pixels for the texture.
    else
    {
        //Get pixel data
        Uint32* pixels = gFooTexture.getPixels32();
        int pixelCount = gFooTexture.getPitch32() * gFooTexture.getHeight();

        //Map colors
        Uint32 colorKey = gFooTexture.mapRGBA( 0xFF, 0x00, 0xFF, 0xFF );
        Uint32 transparent = gFooTexture.mapRGBA( 0xFF, 0xFF, 0xFF, 0x00 );
After the pixels are loaded, we're going to go through the pixels and make all the background pixels transparent. What we're doing is essentially manually color keying the image.

First we want to get a pointer to the pixels and get the number of pixels we're going to traverse. Then we want to map the color key (in this case we're using magenta), and the transparent color.
        //Color key pixels
        for( int i = 0; i < pixelCount; ++i )
        {
            if( pixels[ i ] == colorKey )
            {
                pixels[ i ] = transparent;
            }
        }

        //Create texture from manually color keyed pixels
        if( !gFooTexture.loadFromPixels() )
        {
            printf( "Unable to load Foo' texture from surface!\n" );
        }
    }

    return success;
}
What we're going to do is find all the pixels that are the color key color and then replace them with transparent pixels.

After we're done going through the pixels we load the texture from the pixels we manipulated.
void LTexture::free()
{
    //Free texture if it exists
    if( mTexture != NULL )
    {
        SDL_DestroyTexture( mTexture );
        mTexture = NULL;
        mWidth = 0;
        mHeight = 0;
    }

    //Free surface if it exists
    if( mSurfacePixels != NULL )
    {
        SDL_FreeSurface( mSurfacePixels );
        mSurfacePixels = NULL;
    }
}
Don't forget, whenever you allocate memory always make sure to have a matching free.