GLProgramming.com

home :: about :: development guides :: irc :: forums :: search :: paste :: links :: contribute :: code dump

-> Click here to learn how to get live help <-



 
 

Development Guide Library


Windows bmp file loading and colour-keying by baldurk

Introduction



The purpose of this DG is twofold. One, it details the windows .bmp file format and how to load it into an OpenGL texture. This format does not have an alpha channel and this leads on to the second topic which is using a textures alpha channel to implement colour-keying in OpenGL.

Let's get cracking on the BMP file format. The detailed file format specification is here, but I'll give you a summary as we go along. I'm going to paste the entire loading code here. Some of this (specifically the very end) is not directly related to the bmp loading process, as I'll explain when we get there. Feel free to refer to this as we go through the format.


Sample code



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
 
// somewhere you need to enable blending and set the blending function
//////////////////////////////////////////////////////////


glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

// in your drawing function..
/////////////////////////////

// bind the red texture and draw a textured quad with it

glBindTexture(GL_TEXTURE_2D, textures[RED_TEX]);
glBegin(GL_QUADS);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, 0.0f);
glEnd();

// you'll probably want to do some translation here
//////////////////////#t1#3--
// bind the blue texture and draw a textured quad with it

glBindTexture(GL_TEXTURE_2D, textures[BLUE_TEX]);
glBegin(GL_QUADS);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, 0.0f);
glEnd();



// This function loads a .bmp file and puts it in the GLuint specified
bool LoadBitmap(const char *filename, GLuint tex)
{
    FILE *fp = fopen(filename, "r");
    if(!fp)
    {
	cout << "Could not open the file." << endl;
	return false;
    }

    unsigned short uint;
    unsigned int dword;
    unsigned short word;
    long llong;
    // read some data we don't need
    fread(&uint, sizeof(uint), 1, fp);
    fread(&dword, sizeof(dword), 1, fp);
    fread(&uint, sizeof(uint), 1, fp);
    fread(&uint, sizeof(uint), 1, fp);
    fread(&dword, sizeof(dword), 1, fp);
    fread(&dword, sizeof(dword), 1, fp);

    long width, height;
    fread(&width, sizeof(long), 1, fp);
    fread(&height, sizeof(long), 1, fp);

    fread(&word, sizeof(word), 1, fp);

    unsigned short bitcount;
    fread(&bitcount, sizeof(unsigned short), 1, fp);

    if(bitcount != 24)
    {
	cout << "invalid bitcount. Make a 24bit image." << endl;
	return false;
    }


    unsigned long compression;
    fread(&compression, sizeof(unsigned long), 1, fp);

    if(compression != 0)
    {
	cout << "invalid compression. Make sure the BMP is not using RLE." << endl;
	return false;
    }

    fread(&dword, sizeof(dword), 1, fp);
    fread(&llong, sizeof(long), 1, fp);
    fread(&llong, sizeof(long), 1, fp);
    fread(&dword, sizeof(dword), 1, fp);
    fread(&dword, sizeof(dword), 1, fp);

    unsigned char *imagedata;
    imagedata = new unsigned char[width * height * 4];

    for(int x=0; x < width; x++)
    {
	for(int y=0; y < height; y++)
	{
	    fread(&imagedata[(x * height + y)*4+0], sizeof(char), 1, fp);
	    fread(&imagedata[(x * height + y)*4+1], sizeof(char), 1, fp);

	    fread(&imagedata[(x * height + y)*4+2], sizeof(char), 1, fp);

	    int r = imagedata[(x * height + y)*4+2];
	    int b = imagedata[(x * height + y)*4+0];
	    imagedata[(x * height + y)*4+0] = r;
	    imagedata[(x * height + y)*4+2] = b;

	    if(imagedata[(x * height + y)*4+0] == 255 &&
 	       imagedata[(x * height + y)*4+1] == 0 &&
	       imagedata[(x * height + y)*4+2] == 255)
		imagedata[(x * height + y)*4+3] = 0;
	    else
		imagedata[(x * height + y)*4+3] = 255;
		
	}
    }

    // create your texture here

    delete[] imagedata;
    return true;
}

The windows bmp format



OK, now how do we go about loading a bmp file? well, first thing's first is to load up the file. Once you've loaded up the file, you're ready to start loading in data. But wait, first we've got to decide on the size of different types. In the specification it talks about DWORDs, WORDs, LONGs, UINTs and BYTEs for the various data types. The actual size of the variable types you'll need for these will vary depending on compiler and platform. Your compiler might even have those defined to be the right ones. For the test .bmps that I have a DWORD was 4 bytes, a WORD and UINT were 2 bytes, LONGs were 4 bytes and BYTES are, obviously, 1 byte. The types I used in my code (worked for me, mileage may vary) were DWORD: unsigned int, WORD/UINT: unsigned short, LONG: long and BYTE is unsigned char. You might need to play around with these to make sure you get the right number of bytes read in from your file, etc. Those types are a starting point at least.

First in the bmp structure comes what is known as a "file header". This gives details about the size and layout of the bmp file. We don't actually need any of that information so we can just skip past it. However, in case you wanted to know exactly what we're skipping past, here's some details.

First there's a UINT. This is actually two seperate bytes, which are the character 'B' and the character 'M'. All bmp files must start with this. If you want to verify that you are actually reading a bmp, you can read in the first UINT and check it against 0x424D (0x42 is 'B', 0x4D is 'M'). For simplicity's sake, I haven't done any such check. Next comes a DWORD which is the size of the file in bytes. We don't care how big it is, we calculate how much to read from the width and height of the image. Then come two UINTs which are reserved. They should be 0 and if you want to check for validity again, you can check that they are 0. Then finally in the file header there's a DWORD which is the number of bytes from the end of the file header to the data in the file.

So as you can see, there's not much useful information in that header. So you can just skip past it all any way you like. The next structure is more useful. It's the "Info header" and it contains some details that we're interested in. Here's a break down of what it is:

First we have a DWORD which specifies how many bytes long the info header is. Because this is fixed, we're not too interested so we'll just skip past it. Next come two very important pieces of information: a LONG for the width and a LONG for the height, in that order. We read them in and store them. Next comes a WORD for the number of planes. This is always 1 in bmp files. You can safely ignore this value as you likely won't be using it.

After that comes a WORD called the "bitcount". This will be one of 1, 4, 8 and 24. If it is 1, the image is monochrome. If it is 4 or 8, the image is a 16 or 256 colour image. If it is 4 or 8 then the image contains a palette of RGB values, and the image data is a 4 or 8 bit index into that palette. If it is 24 then there is no palette in the image, and the image data is specified in 3 BYTE triples for RGB values. This is what we're interested in so you'll probably want to give an error if the bitcount is not 24.

We're also interested in the next value. It is a DWORD which specifies how the image is compressed. Because we're not dealing with any type of compression, we want to make sure that this value is 0 (indicating no compression). Other values indicate some type of RLE (run length encoding). If you're interested in supporting compression, you'll want to research how RLE works in general, and how the bmp format implements it.

After that there's a DWORD which specifies how big the image is. This value is really only useful if the image is compressed using RLE, and if it isn't compressed this is likely to be 0 anyway, so we ignore it. Next come two LONGs specifying the resolution of the image, first the horizontal pixels per metre for the device the image is indended for, then one for the vertical dimension. These are useful if you're wanting to choose the most appropriate bitmap for the device you're coding for. We're not doing that, so we'll just ignore it. The next two members (both DWORDs) deal the number of colour indexes in the palette and how many are important. Because we're not dealing with palettised images, we can just ignore them. Note that if you are dealing with palettised images (16 and 256 colour images) you need to read the BMP specification to see exactly what all these values we're missing out do.

That's it for the info header, we're done! Now we can start reading the actual image data. Compared to all that messing with headers, this is the easy bit.

First you want to create an array to hold all your colour data. Because the colour data comes in triples of BYTEs then you should declare it of size width * height * 3 (three BYTEs for each pixel).

Next you should loop through all your pixels, reading out the values in the file into your array of BYTEs. It doesn't matter whether you read row-major or column-major, because all it'll do is rotate/flip your texture meaning you'll need to use different uv co-ordinates. Personally I read column major. One thing to note is that the bmp file format is stored in BGR format, so you'll either want to flip the blue and red BYTEs, or remember that when it comes to calling gluBuild2DMipmaps.

Now that you have your data, you'll just need to call gluBuild2DMipmaps like you would to create any texture, and pass in the data, width and height that you read out of the file (note if you didn't swap the blue and red BYTEs, you'll want to pass in GL_BGR for the format here. Keep the internal format as GL_RGB though).

So that's it! The bmp format is pretty simple as you can see. If you use your code to load up the bmp files I included, however, you'll notice that they have ugly background magenta colours (RGB: 255, 0, 255). The next step is to make those magenta parts transparent. That's where colour-keying comes into play.


Color keying



OpenGL cannot natively select one colour to be transparent (often called colour-keying). You need to provide the alpha data yourself. However, this is amazingly simple. We will modify the above sample to pass in alpha values.

The first thing to do is modify your image data array. You will now need 4 BYTEs for every pixel. The extra BYTE is to represent the alpha value, which indicates whether the pixel is opaque - 255 or transparent - 0 (Note, there is no reason you can't have an alpha of 150 say. That is a little over half-transparent. However in this DG we're just dealing with fully opaque or fully transparent).

When you load in each pixel, you need to check to see what colour it is. If it is magenta (RGB: 255, 0, 255) then you assign the alpha value to be 0. If it is any other colour, you assign the alpha value to be 255. Remember, if the alpha is 0 it is transparent and invisible, if it is 255 then it is fully opaque and visible. This is really easy, some simple code like this would work:



1
2
3
4
5
6
7
8
9
10
11
 
BYTE pixel[4];
if(pixel[0] == 255 &&
   pixel[1] == 0 &&
   pixel[2] == 255)
{
    pixel[3] = 0;
}
else
{
    pixel[3] = 255;
}


Then all you need to do is pass in GL_RGBA (or GL_BGRA) to gluBuild2DMipmaps and you're done! Your texture now has an alpha component so any blending or alpha testing operations will work just fine. (Note: we're going to use blending in this DG).

Ooops. You will notice if you try to render your textures that you still get the magenta backgrounds. What gives?

Well, although there is now an alpha channel in your textures, OpenGL isn't using it. To enable blending (using the texture's alpha channel) you need to put in your init code somewhere the following code snippet:



1
2
 
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);


This enables blending and sets the blending function to only draw those fragments where the alpha is greater than 0 (and to partially draw those with alpha somewhere between 0 and 255). Now if you draw your quads, you'll notice they work fine.

Well... almost. See the next section for details


Conclusions



Well, hopefully you now understand how to load the bmp file format. Detailed specs are here. You should also understand the principle behind colour keying (and how simple it is!).

If you've played around a lot with the examples, you might notice some strange artifacts. If you had depth testing enabled, and one quad in front of another like this:


-----QUAD1-----


      ------QUAD2-----


   \         /
    \       /
     \     /
      \   /
       \ /
       EYE


If you draw these quads in the order QUAD1 then QUAD2, all will be fine and dandy. The reason for this is that the part of QUAD2 that overlaps QUAD1 will blend correctly because the fragments from QUAD1 are in the colour buffer, and when QUAD2 gets drawn over it, the parts that are opaque will occlude QUAD1, but the parts that are transparent will not. However, if you draw them in the order QUAD2, then QUAD1 things will not go as planned. when QUAD1 gets drawn behind QUAD2, the depth test will discard ALL fragments that are behind ALL fragments in QUAD2. This means that even if there is a fragment that is transparent in QUAD2, its depth still gets written into the depth buffer. So when QUAD1 gets drawn behind, even if parts of it might show through, they are discarded because they fail the depth test.

The solution? sort your polygons so that you draw the ones furthest away from the eye first and the ones nearest last. This isn't 100% perfect (in the case of polygons intersecting) but depth-sorting is a large topic and one that is well beyond the scope of this DG.

Well, I hope that helped some of you out with something. If you need any help with anything feel free to email me

- baldurk