Texture Blitting and Texture Padding
Last Updated: Oct 4th, 2014
Blitting is the process of copying pixels from one image to another. In this tutorial, we're going to two halves of an image and combine them by blitting the pixel data. We'll also have to manually pad the pixel, since DevIL can't do it for us this time.From LTexture.h
void createPixels32( GLuint imgWidth, GLuint imgHeight ); /* Pre Condition: -A valid OpenGL context Post Condition: -Frees existing texture data -Allocates 32bit pixel data for member pixels Side Effects: -None */ void copyPixels32( GLuint* pixels, GLuint imgWidth, GLuint imgHeight ); /* Pre Condition: -A valid OpenGL context Post Condition: -Copies given pixel data into member pixels Side Effects: -None */ void padPixels32(); /* Pre Condition: -Valid 32bit member pixels Post Condition: -Takes current member pixel data and gives it power of two dimensions Side Effects: -None */
Here are some new 32bit functions to do some new pixel operations. createPixels32() creates a blank set of pixels for us to use, copyPixels32() copies a set of external pixels, and
padPixels32() pads the member pixels to be power of two.
From LTexture.h
void createPixels8( GLuint imgWidth, GLuint imgHeight ); /* Pre Condition: -A valid OpenGL context Post Condition: -Frees existing texture data -Allocates 8bit pixel data for member pixels Side Effects: -None */ void copyPixels8( GLubyte* pixels, GLuint imgWidth, GLuint imgHeight ); /* Pre Condition: -A valid OpenGL context Post Condition: -Copies given pixel data into member pixels Side Effects: -None */ void padPixels8(); /* Pre Condition: -Valid 8bit member pixels Post Condition: -Takes current member pixel data and gives it power of two dimensions Side Effects: -None */
Here's the 8bit versions of the previously mention functions.
From LTexture.h
void blitPixels32( GLuint x, GLuint y, LTexture& destination ); /* Pre Condition: -Available 32bit member and destination pixels -Valid blitting coordinates Post Condition: -Copies member pixels to destination member pixels at given location -Function will misbehave if invalid blitting coordinate or image dimensions are used Side Effects: -None */ void blitPixels8( GLuint x, GLuint y, LTexture& destination ); /* Pre Condition: -Available 8bit member and destination pixels -Valid blitting coordinates Post Condition: -Copies member pixels to destination member pixels at given location -Function will misbehave if invalid blitting coordinate or image dimensions are used Side Effects: -None */
Here are the 32bit and 8bit pixel blitting functions. They copy the image from the member pixels onto the destination LTexture's member pixels.
From LTexture.cpp
void LTexture::createPixels32( GLuint imgWidth, GLuint imgHeight ) { //Valid dimensions if( imgWidth > 0 && imgHeight > 0 ) { //Get rid of any current texture data freeTexture(); //Create pixels GLuint size = imgWidth * imgHeight; mPixels32 = new GLuint[ size ]; //Copy pixel data mImageWidth = imgWidth; mImageHeight = imgHeight; mTextureWidth = mImageWidth; mTextureHeight = mImageWidth; //Set pixel formal mPixelFormat = GL_RGBA; } }
The function createPixels32() is pretty simple. First it makes sure that a valid set of image dimensions were given. Then it frees any existing texture data because we don't want
to create a conflict between new and previous texture data.
After that, we allocate the pixel data and set the LTexture attributes.
After that, we allocate the pixel data and set the LTexture attributes.
From LTexture.cpp
void LTexture::copyPixels32( GLuint* pixels, GLuint imgWidth, GLuint imgHeight ) { //Pixels have valid dimensions if( imgWidth > 0 && imgHeight > 0 ) { //Get rid of any current texture data freeTexture(); //Copy pixels GLuint size = imgWidth * imgHeight; mPixels32 = new GLuint[ size ]; memcpy( mPixels32, pixels, size * 4 ); //Copy pixel data mImageWidth = imgWidth; mImageHeight = imgHeight; mTextureWidth = mImageWidth; mTextureHeight = mImageWidth; //Set pixel format mPixelFormat = GL_RGBA; } }
copyPixels32() works similarly to createPixels32() only it does a memcpy() on an external pixel pointer. Since this is RGBA pixel data with 4 bytes per pixel, the size of the
memory being copied is number of pixels times four.
From LTexture.cpp
void LTexture::blitPixels32( GLuint x, GLuint y, LTexture& destination ) { //There are pixels to blit if( mPixels32 != NULL && destination.mPixels32 != NULL ) { //Copy pixels rows GLuint* dPixels = destination.mPixels32; GLuint* sPixels = mPixels32; for( int i = 0; i < mImageHeight; ++i ) { memcpy( &dPixels[ ( i + y ) * destination.mTextureWidth + x ], &sPixels[ i * mTextureWidth ], mImageWidth * 4 ); } } }
Before we go over padPixels32(), let's go over blitPixels32() and how pixel blitting works at a byte level.
First we make sure there are pixels to blit with. After that we have a call to memcpy() that copies "sPixels" (source pixels) to "dPixels" (destination pixels) row by row.
Now how do the equations in memcpy() work? Let's say we wanted to copy the pixels on the left to the pixels on the right at the bottom right corner:
We're blitting the pixels in the top left corner so the offset is x = 1, y = 1. For the first iteration of the loop "i" will equal 0. This means for the first loop we'll get:
dPixels[ ( 0 + 1 ) * 4 + 1 ] and sPixels[ 0 * 3 ]
So the first row of 3 sPixels gets copied starting at dPixels[ 5 ] which is what you would expect.
For the second iteration of the loop i = 1, x = 1, and y = 1. This gives us:
dPixels[ ( 1 + 1 ) * 4 + 1 ] and sPixels[ 1 * 3 ]
So source pixels starts copying at 3 to destination pixels at 9. I'm sure you could figure out the last row.
Two things to note: 1) Remember that since it's 32bit pixels, we copy imageWidth * 4 bytes per pixels when copying pixel rows. 2) Also it's the texture width not the image width that we use to calculate how many pixels there are per row.
First we make sure there are pixels to blit with. After that we have a call to memcpy() that copies "sPixels" (source pixels) to "dPixels" (destination pixels) row by row.
Now how do the equations in memcpy() work? Let's say we wanted to copy the pixels on the left to the pixels on the right at the bottom right corner:
We're blitting the pixels in the top left corner so the offset is x = 1, y = 1. For the first iteration of the loop "i" will equal 0. This means for the first loop we'll get:
dPixels[ ( 0 + 1 ) * 4 + 1 ] and sPixels[ 0 * 3 ]
So the first row of 3 sPixels gets copied starting at dPixels[ 5 ] which is what you would expect.
For the second iteration of the loop i = 1, x = 1, and y = 1. This gives us:
dPixels[ ( 1 + 1 ) * 4 + 1 ] and sPixels[ 1 * 3 ]
So source pixels starts copying at 3 to destination pixels at 9. I'm sure you could figure out the last row.
Two things to note: 1) Remember that since it's 32bit pixels, we copy imageWidth * 4 bytes per pixels when copying pixel rows. 2) Also it's the texture width not the image width that we use to calculate how many pixels there are per row.
From LTexture.cpp
void LTexture::padPixels32() { //If there are pixels to pad if( mPixels32 != NULL ) { //Old texture attributes GLuint oTextureWidth = mTextureWidth; GLuint oTextureHeight = mTextureHeight; //Calculate power of two dimensions mTextureWidth = powerOfTwo( mImageWidth ); mTextureHeight = powerOfTwo( mImageHeight ); //The bitmap needs to change if( mTextureWidth != mImageWidth || mTextureHeight != mImageHeight ) { //Allocate pixels GLuint size = mTextureWidth * mTextureHeight; GLuint* pixels = new GLuint[ size ]; //Copy pixels rows for( int i = 0; i < mImageHeight; ++i ) { memcpy( &pixels[ i * mTextureWidth ], &mPixels32[ i * oTextureWidth ], mImageWidth * 4 ); } //Change pixels delete[] mPixels32; mPixels32 = pixels; } } }
For padPixels32(), all we do is create 32bit pixel data that is power of two, blit the current pixel data onto the new pixel data, delete the old data and swap the pointers.
From LTexture.cpp
void LTexture::createPixels8( GLuint imgWidth, GLuint imgHeight ) { //Valid dimensions if( imgWidth > 0 && imgHeight > 0 ) { //Get rid of any current texture data freeTexture(); //Create pixels GLuint size = imgWidth * imgHeight; mPixels8 = new GLubyte[ size ]; //Copy pixel data mImageWidth = imgWidth; mImageHeight = imgHeight; mTextureWidth = mImageWidth; mTextureHeight = mImageWidth; //Set pixel format mPixelFormat = GL_ALPHA; } } void LTexture::copyPixels8( GLubyte* pixels, GLuint imgWidth, GLuint imgHeight ) { //Pixels have valid dimensions if( imgWidth > 0 && imgHeight > 0 ) { //Get rid of any current texture data freeTexture(); //Copy pixels GLuint size = imgWidth * imgHeight; mPixels8 = new GLubyte[ size ]; memcpy( mPixels8, pixels, size ); //Copy pixel data mImageWidth = imgWidth; mImageHeight = imgHeight; mTextureWidth = mImageWidth; mTextureHeight = mImageWidth; //Set pixel format mPixelFormat = GL_ALPHA; } } void LTexture::padPixels8() { //If there are pixels to pad if( mPixels8 != NULL ) { //Old texture attributes GLuint oTextureWidth = mTextureWidth; GLuint oTextureHeight = mTextureHeight; //Calculate power of two dimensions mTextureWidth = powerOfTwo( mImageWidth ); mTextureHeight = powerOfTwo( mImageHeight ); //The bitmap needs to change if( mTextureWidth != mImageWidth || mTextureHeight != mImageHeight ) { //Allocate pixels GLuint size = mTextureWidth * mTextureHeight; GLubyte* pixels = new GLubyte[ size ]; //Copy pixels rows for( int i = 0; i < mImageHeight; ++i ) { memcpy( &pixels[ i * mTextureWidth ], &mPixels8[ i * oTextureWidth ], mImageWidth ); } //Change pixels delete[] mPixels8; mPixels8 = pixels; } } } void LTexture::blitPixels8( GLuint x, GLuint y, LTexture& destination ) { //There are pixels to blit if( mPixels8 != NULL && destination.mPixels8 != NULL ) { //Copy pixels rows GLubyte* dPixels = destination.mPixels8; GLubyte* sPixels = mPixels8; for( int i = 0; i < mImageHeight; ++i ) { memcpy( &dPixels[ ( i + y ) * destination.mTextureWidth + x ], &sPixels[ i * mTextureWidth ], mImageWidth ); } } }
The 8bit versions of these functions work the same except we're using 8bit pointers and memcopy() only copies one byte per pixel.
From LTexture.cpp
//Loaded textures LTexture gLeft; LTexture gRight; //Generated combined texture LTexture gCombined;
At the top of LUtil.cpp we declare the two textures we're going to load and the texture we're going to blit them to.
From LUtil.cpp
bool loadMedia() { //Load left texture if( !gLeft.loadPixelsFromFile32( "22_texture_blitting_and_texture_padding/left.png" ) ) { printf( "Unable to load left texture!\n" ); return false; } //Load right texture if( !gRight.loadPixelsFromFile32( "22_texture_blitting_and_texture_padding/right.png" ) ) { printf( "Unable to load right texture!\n" ); return false; } //Create blank pixels gCombined.createPixels32( gLeft.imageWidth() + gRight.imageWidth(), gLeft.imageHeight() );
In loadMedia(), we load the two images as pixels and allocate pixel data large enough to hold both of them.
From LUtil.cpp
//Blit images gLeft.blitPixels32( 0, 0, gCombined ); gRight.blitPixels32( gLeft.imageWidth(), 0, gCombined ); //Pad and create texture gCombined.padPixels32(); gCombined.loadTextureFromPixels32(); //Get rid of old textures gLeft.freeTexture(); gRight.freeTexture(); return true; }
Then we blit the two images onto the blank pixels, pad the pixels so they're power of two, and create the texture.
With both the images blitted onto a new texture, we free the old images.
With both the images blitted onto a new texture, we free the old images.
From LUtil.cpp
void render() { //Clear color buffer glClear( GL_COLOR_BUFFER_BIT ); glLoadIdentity(); //Render combined texture gCombined.render( ( SCREEN_WIDTH - gCombined.imageWidth() ) / 2.f, ( SCREEN_HEIGHT - gCombined.imageHeight() ) / 2.f ); //Update screen glutSwapBuffers(); }
And finally, we render the generated texture.