Non-Power-of-Two Textures
Last Updated: Aug 9th, 2012
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.