Loading Transparent PNGs in OpenGL for Dummies! - Thu, Jun 30, 2011
I’m writing this because as far as I can remember, nothing has taken my time this much. I woke up yesterday and began working on a PNG Importer for my new OpenGL/C++ based side scroller game engine which I’m going to use to make my first iOS games, and after tons of research I realized it’s three in the morning and I haven’t yet figured out how to load transparent PNGs in my game.
Okay, let’s back off for a sec.
Previously I wrote I’m going to give something called AirPlay SDK a try and write my iOS games with that. My reason was to skip Objective-C and be able to make a cross-platform code that works on Android as well. I looked at some of the examples for AirPlay SDK and found it very cryptic. That’s when I told myself “Screw it, Let’s go with Objective-C like the rest of the world.”
So I set up a Mac OS X virtual machine and created a test iOS project to see how writing Objective-C feels like. It’s actually a pretty straightforward language (even though it feels very very uncomfortable for those who had experience with C++-like languages the first time they see it), and you can write normal C code in it with no problems. Sweet.
So again I was getting comfy choosing to write my code in Objective-C, but then I learned that if I rename my source files from .m to .mm, the compiler compiles them as something it calls Objective-C++ which allows me to write my engine in C++! But with a few limitations of course:
- A C++ class cannot derive from an Objective-C class and vice versa.
- C++ namespaces cannot be declared inside an Objective-C declaration.
- Objective-C classes cannot have instance variables of C++ classes that do not have a default constructor or that have one or more virtual methods, but pointers to C++ objects can be used as instance variables without restriction (allocate them with new in the -init method).
- C++ “by value” semantics cannot be applied to Objective-C objects, which are only accessible through pointers.
- An Objective-C declaration cannot be within a C++ template declaration and vice versa. However, Objective-C types, (e.g., Classname *) can be used as C++ template parameters.
- Objective-C and C++ exception handling is distinct; the handlers of each cannot handle exceptions of the other type.
- Care must be taken since the destructor calling conventions of Objective-C and C++’s exception run-time models do not match (i.e., a C++ destructor will not be called when an Objective-C exception exits the C++ object’s scope). The new 64-bit runtime resolves this by introducing interoperability with C++ exceptions in this sense.
Basically it means that you can write your C++ code and Objective-C code in the same file, but you can’t mix their stuff together. So for example I have to connect my engine code which is going to be written in C++ to another Interface code which handles the iOS stuff and is written in Objective-C. It’s gonna be a little bit messy, but it gives me the freedom to write my main code in C++, which is pretty cool.
Being able to write my engine in regular C++ gives me the advantage to write my code on Windows and Visual Studio and makes it much easier to test. So I made a Win32 project and began writing an OpenGL based engine.
So, back to the PNG stuff…
There are lots of tutorials on importing TGA files on OpenGL, but TGA is a crappy format. I chose to go with PNG because it’s way easier to work with. So I began searching for docs and guides and tutorials on Importing PNG files in OpenGL. I found libpng which seems to be the official SDK for working with PNGs.
I downloaded the source code package, which contains a lot of source files and is written in C, and copied all of them to my project Smile. Then I realized that libpng needs another library called zlib to function. So I downloaded zlib’s source files and again copied them all to my project. This is of course not the right way to use another library in a project, but I wanted my code to be as portable as possible. whatever.
I wasn’t in the mood to read all those docs for libpng to figure out a task as simple as reading PNGs and converting them to raw bytes in the memory. So I tried to find a sample code that does this instead. I found this in wikibooks:
#include <GL/gl.h>
#include <GL/glu.h>
#include <png.h>
#include <cstdio>
#include <string>
#define TEXTURE_LOAD_ERROR 0
using namespace std;
/** loadTexture
* loads a png file into an opengl texture object, using cstdio , libpng, and opengl.
*
* param filename : the png file to be loaded
* param width : width of png, to be updated as a side effect of this function
* param height : height of png, to be updated as a side effect of this function
*
* return GLuint : an opengl texture id. Will be 0 if there is a major error,
* should be validated by the client of this function.
*
*/
GLuint loadTexture(const string filename, int &width, int &height)
{
//header for testing if it is a png
png_byte header[8];
//open file as binary
FILE *fp = fopen(filename.c_str(), "rb");
if (!fp) {
return TEXTURE_LOAD_ERROR;
}
//read the header
fread(header, 1, 8, fp);
//test if png
int is_png = !png_sig_cmp(header, 0, 8);
if (!is_png) {
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
//create png struct
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL,
NULL, NULL);
if (!png_ptr) {
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//create png info struct
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, (png_infopp) NULL, (png_infopp) NULL);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//create png info struct
png_infop end_info = png_create_info_struct(png_ptr);
if (!end_info) {
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//png error stuff, not sure libpng man suggests this.
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//init png reading
png_init_io(png_ptr, fp);
//let libpng know you already read the first 8 bytes
png_set_sig_bytes(png_ptr, 8);
// read all the info up to the image data
png_read_info(png_ptr, info_ptr);
//variables to pass to get info
int bit_depth, color_type;
png_uint_32 twidth, theight;
// get info about png
png_get_IHDR(png_ptr, info_ptr, &twidth, &theight, &bit_depth, &color_type,
NULL, NULL, NULL);
//update width and height based on png info
width = twidth;
height = theight;
// Update the png info struct.
png_read_update_info(png_ptr, info_ptr);
// Row size in bytes.
int rowbytes = png_get_rowbytes(png_ptr, info_ptr);
// Allocate the image_data as a big block, to be given to opengl
png_byte *image_data = new png_byte[rowbytes * height];
if (!image_data) {
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
//row_pointers is for pointing to image_data for reading the png with libpng
png_bytep *row_pointers = new png_bytep[height];
if (!row_pointers) {
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
delete[] image_data;
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
// set the individual row_pointers to point at the correct offsets of image_data
for (int i = 0; i < height; ++i)
row_pointers[height - 1 - i] = image_data + i * rowbytes;
//read the png into image_data through row_pointers
png_read_image(png_ptr, row_pointers);
//Now generate the OpenGL texture object
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*) image_data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
delete[] image_data;
delete[] row_pointers;
fclose(fp);
return texture;
}
The image_data
pointer in this code contains what I need. With a single glDrawPixels
call I could draw the image on the screen.
But there’s a tiny problem. libpng is very big. I had to add tons of files to my project to be able to render a tiny image. So I found another library called lodepng. It’s made of only two files: A source file and a header, and can load the image with no problems. It’s also easier to understand since it’s written in C++ instead of C. You can download the files from here.
So here’s how to load a PNG into memory using lodepng:
- Add lodepng.h and lodepng.cpp to your project.
- Load the PNG using LodePNG::LoadFile( vector, std::string ) function.
- Decode your data using LodePNG::Decoder class.
- Your image is now ready to be rendered. You might have to flip it though.
This code can get you going:
GLboolean NTexture::load(string path)
{
std::vector rawImage;
LodePNG::loadFile( rawImage, path );
LodePNG::Decoder decoder;
std::vector image;
decoder.decode( image, rawImage.empty() ? 0 : &rawImage[0],
(unsigned)rawImage.size() );
size.x = decoder.getWidth();
size.y = decoder.getHeight();
imageSize = image.size();
colorDepth = decoder.getBpp();
format = GL_RGBA;
type = GL_UNSIGNED_BYTE;
pixmap = new GLubyte[imageSize];
//
// Flip and invert the PNG image since OpenGL likes to load everything
// backwards from what is considered normal!
//
unsigned char *imagePtr = &image[0];
int halfTheHeightInPixels = decoder.getHeight() / 2;
int heightInPixels = decoder.getHeight();
// Assuming RGBA for 4 components per pixel.
int numColorComponents = 4;
// Assuming each color component is an unsigned char.
int widthInChars = decoder.getWidth() * numColorComponents;
unsigned char *top = NULL;
unsigned char *bottom = NULL;
unsigned char temp = 0;
for( int h = 0; h < halfTheHeightInPixels; ++h )
{
top = imagePtr + h * widthInChars;
bottom = imagePtr + (heightInPixels - h - 1) * widthInChars;
for( int w = 0; w < widthInChars; ++w )
{
// Swap the chars around.
temp = *top;
*top = *bottom;
*bottom = temp;
++top;
++bottom;
}
}
unsigned int c=0;
for (unsigned int x = 0; x < size.x; x++)
for (unsigned int y=0; y<size.y; y++)
for (unsigned int co=0; co<numColorComponents; co++)
((GLubyte*)pixmap)[c++]=image[c];
return TRUE;
}
To be able to render transparent PNGs, you have to enable blending like this:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);