Non-Power-of-Two Textures

Last Updated 8/09/12
In this tutorial, we'll render a 520x235 image by padding it so it's a 1024x256 texture.From LTexture.h
bool loadTextureFromPixels32( GLuint* pixels, GLuint imgWidth, GLuint imgHeight, GLuint texWidth, GLuint texHeight );
/*
Pre Condition:
-A valid OpenGL context
Post Condition:
-Creates a texture from the given pixels
-Reports error to console if texture could not be created
Side Effects:
-Binds a NULL texture
*/
Here's our tweaked loadTextureFromPixels32() function.
The way we're going to render a non-power-of-two texture (say 520x120) is by making the image bigger by padding it with pixels (to say 512x128) and then clipping out the portion of the texture with the actual image on it.
To do that, we need to know how big the original image is and how big the padded texture is.
The way we're going to render a non-power-of-two texture (say 520x120) is by making the image bigger by padding it with pixels (to say 512x128) and then clipping out the portion of the texture with the actual image on it.
To do that, we need to know how big the original image is and how big the padded texture is.
From LTexture.h
private:
GLuint powerOfTwo( GLuint num );
/*
Pre Condition:
-None
Post Condition:
-Returns nearest power of two integer that is greater
Side Effects:
-None
*/
//Texture name
GLuint mTextureID;
//Texture dimensions
GLuint mTextureWidth;
GLuint mTextureHeight;
//Unpadded image dimensions
GLuint mImageWidth;
GLuint mImageHeight;
This means that we need variables in the LTexture class to store both the original image dimensions, and padded texture dimensions.
I also wanted to point out our powerOfTwo() function that we'll be using to calculate how much we need to pad the texture. If we give it an argument of 60, it will return 64.
I also wanted to point out our powerOfTwo() function that we'll be using to calculate how much we need to pad the texture. If we give it an argument of 60, it will return 64.
From LTexture.cpp
#include "LTexture.h"
#include <IL/il.h>
#include <IL/ilu.h>
LTexture::LTexture()
{
//Initialize texture ID
mTextureID = 0;
//Initialize image dimensions
mImageWidth = 0;
mImageHeight = 0;
//Initialize texture dimensions
mTextureWidth = 0;
mTextureHeight = 0;
}
At the top of LTexture.cpp, we include ilu because we're going to be using DevIL utilities.
Also in our constructor, we initialize both sets of texture dimensions.
Also in our constructor, we initialize both sets of texture dimensions.
From LTexture.cpp
bool LTexture::loadTextureFromFile( std::string path )
{
//Texture loading success
bool textureLoaded = 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 RGBA
success = ilConvertImage( IL_RGBA, IL_UNSIGNED_BYTE );
if( success == IL_TRUE )
{
//Initialize dimensions
GLuint imgWidth = (GLuint)ilGetInteger( IL_IMAGE_WIDTH );
GLuint imgHeight = (GLuint)ilGetInteger( IL_IMAGE_HEIGHT );
In loadTextureFromFile(), we load and convert our image as usual. This time however, we make sure to get the original image width/height.
From LTexture.cpp
//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 );
}
After loading our image, we calculate how big we have to make the image so it's a power of two texture using our powerOfTwo() function.
Then we check if the required dimensions are not equal to current dimensions. If the loaded image is already power of two, there's no reason to resize it.
If the image does need to be resized, we set the image origin at the top left using iluImageParameter() so when the image is padded it add pixels to the bottom/right portion of the image. Then to pad the image, we enlarge the canvas to the required texture dimensions using iluEnlargeCanvas().
Also, when it pads the image it pads it with the color specified with ilClearColour().
Then we check if the required dimensions are not equal to current dimensions. If the loaded image is already power of two, there's no reason to resize it.
If the image does need to be resized, we set the image origin at the top left using iluImageParameter() so when the image is padded it add pixels to the bottom/right portion of the image. Then to pad the image, we enlarge the canvas to the required texture dimensions using iluEnlargeCanvas().
Also, when it pads the image it pads it with the color specified with ilClearColour().
From LTexture.cpp
//Create texture from file pixels
textureLoaded = loadTextureFromPixels32( (GLuint*)ilGetData(), imgWidth, imgHeight, texWidth, texHeight );
}
//Delete file from memory
ilDeleteImages( 1, &imgID );
}
//Report error
if( !textureLoaded )
{
printf( "Unable to load %s\n", path.c_str() );
}
return textureLoaded;
}
With our padded image pixels ready, we send them to our tweaked loadTextureFromPixels32().
From LTexture.cpp
bool LTexture::loadTextureFromPixels32( GLuint* pixels, GLuint imgWidth, GLuint imgHeight, GLuint texWidth, GLuint texHeight )
{
//Free texture if it exists
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 );
//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;
}
return true;
}
Our texture generation function is pretty much the same as before only now it sets both image dimensions and texture dimensions. Which makes sense because clipping out the padded
pixels is part of rendering.
From LTexture.cpp
void LTexture::render( GLfloat x, GLfloat y, LFRect* clip )
{
//If the texture exists
if( mTextureID != 0 )
{
//Remove any previous transformations
glLoadIdentity();
//Texture coordinates
GLfloat texTop = 0.f;
GLfloat texBottom = (GLfloat)mImageHeight / (GLfloat)mTextureHeight;
GLfloat texLeft = 0.f;
GLfloat texRight = (GLfloat)mImageWidth / (GLfloat)mTextureWidth;
//Vertex coordinates
GLfloat quadWidth = mImageWidth;
GLfloat quadHeight = mImageHeight;
As expected, when we set our default texture coordinates, we don't have it so it renders the full 0 to 1 texture. We set to to be the portion of the padded texture that contains
the original image.
From LTexture.cpp
//Handle clipping
if( clip != NULL )
{
//Texture coordinates
texLeft = clip->x / mTextureWidth;
texRight = ( clip->x + clip->w ) / mTextureWidth;
texTop = clip->y / mTextureHeight;
texBottom = ( clip->y + clip->h ) / mTextureHeight;
//Vertex coordinates
quadWidth = clip->w;
quadHeight = clip->h;
}
//Move to rendering point
glTranslatef( x, y, 0.f );
//Set texture ID
glBindTexture( GL_TEXTURE_2D, mTextureID );
//Render textured quad
glBegin( GL_QUADS );
glTexCoord2f( texLeft, texTop ); glVertex2f( 0.f, 0.f );
glTexCoord2f( texRight, texTop ); glVertex2f( quadWidth, 0.f );
glTexCoord2f( texRight, texBottom ); glVertex2f( quadWidth, quadHeight );
glTexCoord2f( texLeft, texBottom ); glVertex2f( 0.f, quadHeight );
glEnd();
}
}
Now when clipping your own portion of the texture, it works just the same as before.
From LTexture.cpp
void LTexture::freeTexture()
{
//Delete texture
if( mTextureID != 0 )
{
glDeleteTextures( 1, &mTextureID );
mTextureID = 0;
}
mImageWidth = 0;
mImageHeight = 0;
mTextureWidth = 0;
mTextureHeight = 0;
}
Our destructor is pretty much the same, only now it resets the sets of dimensions.
From LTexture.cpp
GLuint LTexture::powerOfTwo( GLuint num )
{
if( num != 0 )
{
num--;
num |= (num >> 1); //Or first 2 bits
num |= (num >> 2); //Or next 2 bits
num |= (num >> 4); //Or next 4 bits
num |= (num >> 8); //Or next 8 bits
num |= (num >> 16); //Or next 16 bits
num++;
}
return num;
}
And here's our function we use to calculate the next power of two. Don't worry to much about this bitwise mumbo jumbo. Just know it's a fast way to get the next power of two.
From LUtil.cpp
#include "LUtil.h"
#include <IL/il.h>
#include <IL/ilu.h>
#include "LTexture.h"
//Non-power-of-two texture
LTexture gNon2NTexture;
bool initGL()
{
//Set the viewport
glViewport( 0.f, 0.f, SCREEN_WIDTH, SCREEN_HEIGHT );
//Initialize Projection Matrix
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
glOrtho( 0.0, SCREEN_WIDTH, SCREEN_HEIGHT, 0.0, 1.0, -1.0 );
//Initialize Modelview Matrix
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
//Initialize clear color
glClearColor( 0.f, 0.f, 0.f, 1.f );
//Enable texturing
glEnable( GL_TEXTURE_2D );
//Check for error
GLenum error = glGetError();
if( error != GL_NO_ERROR )
{
printf( "Error initializing OpenGL! %s\n", gluErrorString( error ) );
return false;
}
//Initialize DevIL and DevILU
ilInit();
iluInit();
ilClearColour( 255, 255, 255, 000 );
//Check for error
ILenum ilError = ilGetError();
if( ilError != IL_NO_ERROR )
{
printf( "Error initializing DevIL! %s\n", iluErrorString( ilError ) );
return false;
}
return true;
}
The top of LUtil.cpp has much of the same with the important difference of a call to iluInit(). The functions we used to pad the image ( iluImageParameter() and
iluEnlargeCanvas() ) are part of the DevIL utility functions so we need to init DevIL utilities to use them.
From LUtil.cpp
bool loadMedia()
{
//Load texture
if( !gNon2NTexture.loadTextureFromFile( "08_non_power_of_2_textures/opengl.png" ) )
{
printf( "Unable to load non-power-of-two texture!\n" );
return false;
}
return true;
}
void update()
{
}
void render()
{
//Clear color buffer
glClear( GL_COLOR_BUFFER_BIT );
//Render OpenGL texture
gNon2NTexture.render( ( SCREEN_WIDTH - gNon2NTexture.imageWidth() ) / 2.f, ( SCREEN_HEIGHT - gNon2NTexture.imageHeight() ) / 2.f );
//Update screen
glutSwapBuffers();
}
As usual loadMedia() loads our texture and render() renders it to the screen.
Now taking a 520x235 image and by padding it so it's a 1024x256 texture wastes a lot of pixels. This is why it's good to put multiple images on one texture to use GPU memory as efficiently as possible.
Now taking a 520x235 image and by padding it so it's a 1024x256 texture wastes a lot of pixels. This is why it's good to put multiple images on one texture to use GPU memory as efficiently as possible.