Skip to main content
GameDev.net gamedev.net
🔒 Locked

24 to 16 bitmap converter

Started by Mardigin Mar 8, 2002 at 6:17 PM 27 replies 47.1k views
Original Post
Mardigin
Mardigin
I am trying to write some code that will take a 24bit bmp and convert it to a 16bit bmp. Most of what I have is from Andre LaMothe''s "Tricks of the Game Programming Gurus"...
  
void Bitmap_Convert(BITMAP_FILE_PTR bitmap, UCHAR * buffer16bit)
{
	int n_padding = 0;
	int image_size = (bitmap->bitmapinfoheader.biSizeImage)/3;
	
	UCHAR	blue	= NULL, 
			green	= NULL,
			red		= NULL;
	USHORT	color	= NULL;

	for(int index = 0; index<=image_size; index++)
	{
		n_padding = index * 3;

		blue	= (bitmap->buffer[n_padding]);
		green	= (bitmap->buffer[n_padding + 1]);
		red		= (bitmap->buffer[n_padding + 2]);

		//color += _RGB16BIT565(red, green, blue);

		color = _RGB16BIT565(red, green, blue);
	}

	buffer16bit = (UCHAR *)color;
}
  
Obviously this is not complete. The RGB16BIT565 macro looks like this...
  #define _RGB16BIT565(r,gb) ((b%32) + ((g%64) << 6) + ((r%32) << 11))  
so as far as my understanding goes that macro creates a 16 bit color word. Now I know that in the code above I am not getting all the info I need into the USHORT color because it is in a for loop and I am just writing over itself with each iteration. Now an obvious solution would be to use an array. What I am not sure how to do though is if I do make a USHORT array how do I pass all of the data from the array to my UCHAR * buffer16bit? Am I going about this the best way? Am I on the right track? I got this code on page 355 of Andre LaMothe''s "Tricks of the Game Programming Gurus"... He actually says there that he will be sure to write a 24-bit to 16-bit bitmap converter, but I cannot seem to find it anywhere.
Hyatus
Hyatus
Actually, shouldn''t the 16bit buffer be a USHORT?
I believe UCHAR is used for an 8-bit buffer.

Oh, and I haven''t read that book, but the example is probably on
the accompanied cd.

-Hyatus
"da da da"
Big Sassy
Big Sassy
I remember when I was trying to learn how to do this. Such a pain in the ass. I'll try and give a clear explanation on how to do it. Hopefully I'll do a good job. And one more thing. This will be focusing on 565 pixels only! Most video cards nowadays use 565 pixels for 16bit so it isn't a problem. If you want to learn about the 555 mode, just search the forums. I know someone has discussed the differences between 565 and 555 at some point.

First things first. Forget about LaMothe's code. Not that's it's bad or anything, I just don't want you to get confused. Just disregard it as you read this.

Secondly, download the *.bmp format specifications from www.wotsit.org. You can get it here along with other file formats you can play with Download the one that says "Windows BMP Format (MS Word) [Wim Wouters]" next to it. Take some time to read it.

*.bmp files have 3 sections when they don't have a pallete (8bit bitmaps and lower have palletes). The first is the file header. It's data type is BITMAPFILEHEADER (just like an integer would be of type int). The second one is the info header. It's data type is BITMAPINFOHEADER. And the final section is your pixel data.

So let's start the function. We'll declare some variables to hold our data that we read form the *.bmp file:


    
void LoadBitmap()
{
BITMAPFILEHEADER fileheader;
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
}


Bare in mind that a WORD is the same thing as a USHORT. Each is 16 bits. And a BYTE is the same thing as a UCHAR. Each of those is 8 bits. And finally a DWORD is 32 bits. Don't worry about the varialbe "*bitmapData" for the moment. We'll get to it in a sec.

Anyway, the data type BITMAPFILEHEADER is a struct that holds general information about the bitmap file. It contains what type of bitmap the bitmap is, The total size of the bitmap in bytes, a reserved variable that does nothing (it is reserved for if somebody wants to add information to the header in the future), and the number of bytes the data is from this data structure (not entirely sure about the last one). It looks like this:


              
struct BITMAPFILEHEADER
{
WORD bfType; // type of bitmap

DWORD bfSize; // the total size of the file, in bytes

DWORD bfReserved; // does nothing

DWORD bfOffBits; // amount of bytes there is from the

// beginning of the file to the actual

// pixel data

};


Generally you don't need to worry about anything in that data structure. Everything you really need is in the info header. So let's get rid of the file header we declared:


              
void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
}


Now the info header contains all the good stuff. Stuff we will need to load the bitmap properly. I'll explain the important things inside after I show you the whole structure:


              
struct BITMAPINFOHEADER
{
DWORD biSize; // number of bytes in this structure

LONG biWidth; // width of the bitmap

LONG biHeight; // height of the bitmap

WORD biPlanes; // number of planes (don't worry about it)

WORD biBitCount; // bits per pixel (1,4,8,16,24 or 32)

WORD biCompression; // type of compression

DWORD biSizeImage; // size of image in Bytes

LONG biXPelsPerMeter; // x res of target display

LONG biYPelsPerMeter; // y res of target display

DWORD biClrUsed; // how many colors are used

DWORD biClrImportant; // number of important colors

};


That's a lot of stuff. Luckily you only have to worry about a two things in that. biWidth (which is the width of the bitmap) and biHeight (the height of the bitmap). So it's time to load something. Let's take a look at our function now:


                    
void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
FILE *bitmapFile;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);
}


I know that I'm using C file i/o functions rather than C++, so I'll explain exactly what I'm doing. the line:

bitmapFile = fopen("yourBitmap.bmp", "rb");

Opens the file "yourBitmap.bmp" in read mode. That's what the "r" in "rb" is for. So what is the "b" for? Binary of course. Bitmap files aren't text file so you have to read them in binary mode. I forgot to put that "b" in once and it took me 4 weeks to find out why my function wasn't working. I still kick myself for that one. When fopen opens the file it points the FILE pointer to the file.

Since we don't need any of the information in the BITMAPFILEHEADER we can just skip over that data in the bitmap file. That's what fseek does.

fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);

First the function "fseek" gets the file pointer. Then it gets the amount of bytes that it's skipping. That is what "sizeof(BITMAPFILEHEADER)" is for. Then it gets what position in the file it should move from. "SEEK_SET" is telling it we want to move "sizeof(BITMAPFILEHEADER)" bytes from the beginning of the file.

Then we finally read in the info header (BITMAPINFOHEADER):

fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

"fread" first gets the memory address of where you want to store your data. So we give it "infoheader"'s address. Then we tell it how many bytes we want to read in. Then we tell it we want to read in that many bytes 1 time. Finally we give it the FILE pointer so it know what file we are getting the data from.

Ok. So now we have all the information we need to load in the bitmap. And when we read in the info header of the bitmap, our position in the file is right at the pixel data. Now lets allocate the memory to store the pixel data in our "*bitmapData" variable.


                    
void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
FILE *bitmapFile;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];
}


Lets just say "yourBitmap.bmp" is 200x300 pixels (200 pixels int width and 300 pixels in height). "*bitmapData" now has room for a 16bit bitmap that is 200x300 pixels. Now all it needs are some pixels to put in there. It's time to read in the pixel data from the bitmap.

Since the bitmap your loaded is in 24bit mode, there really isn't a data type that can hold each individual pixel. Everything is read in bytes. A byte is 8bits long. So a 1byte variable has 8bits in it (which would be a UCHAR or a BYTE data type). A 2byte variable would have 16bits in it (which would be a USHORT or a WORD data type). A 3byte varialbe would have 32bits in it (which would be a DWORD data type). As you can see, 24bits fall right in between a WORD and a DWORD. So how do you get each pixel? That's easy.

When a 24bit bitmap is created each pixel's red, green, and blue value are written to the file. Each of those values are 8bits in length, or 1 byte. We can handle 1byte data types. If we read in each red, green, and blue value into it's own UCHAR or BYTE then we'd have a whole pixel, just split into three parts. So lets create those 3 variables.


                    
void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
FILE *bitmapFile;

BYTE red, green, blue;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];
}


We'll use "red", "green", and "blue" to read in the RGB values of each pixel in the 24bit bitmap and convert them to a friendlier 16bit version of the pixel. So lets start reading in some RGB values.


                    
void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
FILE *bitmapFile;

BYTE red, green, blue;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];

for( int y=0; y<infoheader.biHeight; ++y)
{
for( int x=0; x<infoheader.biWidth; ++x)
{
fread(&blue, sizeof(BYTE), 1, file);
fread(&green, sizeof(BYTE), 1, file);
fread(&red, sizeof(BYTE), 1, file);
}
}
}


You may be thinking "Why is he reading the blue value first? I though it was RGB not BGR?" Well, for some reason (which I don't know) bitmaps RGB values are stored backwards in a BGR format. So you read in the blue pixel first, then the green, and THEN the red. Unfortunately, that's not the only backwards thing about bitmaps but we'll get to that soon enough.

As you can see, we are reading in the RGB values for each 24bit pixel, but we aren't doing anything with them. It is now time for me to answer your original question. Converting those values into a 16bit pixel. By the way, if you don't know what makes a binary number then go look it up now. Most of this stuff will go straight over your head if you don't understand binary numbers.

A 24bit pixel looks like this:

8 bits 8 bits 8 bits
11111111 00000000 11111111
red green blue

whereas a 565 16bit pixel looks like this:

5 bits 6 bits 5bits
11111 000000 11111
red green blue

Bare in mind that there could be any value for each RGB value. For example:

10011101 11001101 11000101

could be a 24bit pixel. We'll use this as our example pixel for converting between 16bit and 24bit. Here's our values:

red = 10011101
green = 11001101
blue = 11000101

Unfortunately they are all 8 bits long. In order for them to be compatible 16bit values the red has to be 5 bits long, the green 6 bits, and the blue 5 bits. It's time to introduce you to shifting bits. The operator >> is used in more than just cout and cin. You can use it to shift bits in a value to the left or right. Lets take the number 2 and shift it. In binary it would look like this:

value = 10

Now if we shift it to the left 2 spots it will look like this:

value = 1000

That's great if you want to make a number larger, but we want to make those 24bit RGB values smaller. So instead of shifting to the left we shift to the right. If we shift the last value 3 spaces to the right it will look like this:

value = 1

Now it's only 1 bit long. Now we just need to apply this to those 24bit RGB values. The red and blue need to be 5 bits long and right now they are 8 bits long. Well, 8 - 5 = 3 so we'll shift each of them 3 bits over. And the green needs to be 6 bits long. 8 - 6 = 2 so we'll shift the green over 2 bits. It looks something like this:

Old values:
red = 10011101
green = 11001101
blue = 11001101


                    
red = red >> 3;
green = green >> 2;
blue = blue >> 3;


New values:
red = 10011
green = 110011
blue = 11001

Ok. So we now got 16bit RGB values. Now we just need to put them together to form a 16bit pixel. It's time to use some of those bitwise operator's you've heard so much about. We are going to OR all the pixels together. But first we gotta do something else. We want the values to line up like this.

10011 110011 11001

but right now they line up like this

10011
110011
11001

They aren't lined up correctly at all! Remeber when we shifted that one value (the value 2) to the left earlier? It put 0's in front of the number when it moved it. We are going to do the same thing here. First we'll shift the red into place. There should be 11 0's in front of it. Like this:

10011 000000 00000

and the green needs 5 0's in front of it like this:

110011 00000

The blue is just fine as it is since it's right in the beginning of the 16bit pixel. So lets shift those values over:

Old values:
red = 10011
green = 110011
blue = 11001


                    
red = red << 11;
green = green << 5;
blue = blue;


New values:
red = 10011 000000 00000
green = 110011 00000
blue = 11001

If you line them up they look like this:


                    
10011 000000 00000
110011 00000
11001


Now we just mash them together using OR'ing. If you don't know what OR'ing is then go ahead and look it up real quick. It's not that complicated, but I've used WAY too much space on this post as it stands. So lets OR each value together:

Old values:
red = 10011 000000 00000
green = 110011 00000
blue = 11001


                    
pixel = red | green | blue;


New value:
pixel = 10011 110011 11001

WE FINALLY GOT A 16BIT PIXEL! So now your question has been answered. Now since you understand everything that goes into the macro here's what it would look like.


                    
#define RGB16(red, green, blue) ( ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3))


So now you got an RGB macro. Lets finish off the function for reading in the bitmap. All we have to do now in each pass is get the red, green, and blue values of the 24bit bitmap, convert them to 16bit, and store them in our 16bit bitmap.


      
// Macros

#define RGB16(red, green, blue) ( ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3))

void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
FILE *bitmapFile;

BYTE red, green, blue;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];

for( int y=0; y<infoheader.biHeight; ++y)
{
for( int x=0; x<infoheader.biWidth; ++x)
{
fread(&blue, sizeof(BYTE), 1, file);
fread(&green, sizeof(BYTE), 1, file);
fread(&red, sizeof(BYTE), 1, file);

bitmapData[y*infoheader.biWidth + x] = RGB16(red, green, blue);
}
}
}


We added the line:

bitmapData[y*infoheader.biWidth + x] = RGB16(red, green, blue);

You have to realize that though the bitmap is 200 pixels wide and 300 pixel high, it's really just 60,000 pixels stored in a straight line in memory. Memory isn't 2 dimensional. It goes from the beginning to the end. Just like a line. For example, pixel 0 is the first pixel in the bitmap. Pixel 199 is the last pixel in the first row. Pixel 200 is the first pixel in the second row. Pixel 300 is in the middle of the second row.

So in order to access each line we have to take the line number times the total width of the bitmap. That's what " y*infoheader.biWidth " is for. It takes us to the proper row. The " + x " is to get to the x position on that line. So the first for loop is to go through each line and the second for loop is to get each pixel on that line, convert it, and set it to your 16bit bitmap.

Congratulations! Once that nested for loop is done you have yourself an upside down 16bit bitmap stored in "*bitmapData". You may be thinking "What the hell do you mean upside down?!" Didn't I tell you that the BGR thing wasn't the only thing backwards on a bitmap? The whole friggin image is upside down! I think there was a good reason for this at some time, but right now it's just a large pain in the ass.

But don't worry it's pretty easy to flip back around. We'll just need to write the data to another buffer upside down, thus making it right side up. Lets make another buffer that will hold the image rightside up and allocate the proper memory for it.


    
// Macros

#define RGB16(red, green, blue) ( ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3))

void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
WORD *bitmapDone;
FILE *bitmapFile;

BYTE red, green, blue;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];
bitmapDone = new WORD[infoheader.biWidth * infoheader.biHeight];


for( int y=0; y<infoheader.biHeight; ++y)
{
for( int x=0; x<infoheader.biWidth; ++x)
{
fread(&blue, sizeof(BYTE), 1, file);
fread(&green, sizeof(BYTE), 1, file);
fread(&red, sizeof(BYTE), 1, file);

bitmapData[y*infoheader.biWidth + x] = RGB16(red, green, blue);
}
}
}


Ok. Now we got a place to store the right side up bitmap. Luckily we don't have to convert from 16bit to 16bit so all we have to do is start from the bottom of the upside down bitmap and read the data to the new bitmap from the top. It should look something like this:


                  
// Macros

#define RGB16(red, green, blue) ( ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3))

void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
WORD *bitmapData;
WORD *bitmapDone;
FILE *bitmapFile;

BYTE red, green, blue;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];
bitmapDone = new WORD[infoheader.biWidth * infoheader.biHeight];


for( int y=0; y<infoheader.biHeight; ++y)
{
for( int x=0; x<infoheader.biWidth; ++x)
{
fread(&blue, sizeof(BYTE), 1, file);
fread(&green, sizeof(BYTE), 1, file);
fread(&red, sizeof(BYTE), 1, file);

bitmapData[y*infoheader.biWidth + x] = RGB16(red, green, blue);
}
}

int heightIndex = 0;
for( y=infoheader.biHeight-1; y>=0; --y)
{
for( int x=0; x<infoheader.biWidth; ++x)
bitmapDone[heightIndex*infoheader.biWidth + x] = bitmapData[y*infoheader.biWidth + x];

++heightIndex;
}
}


We set y equal to the last row of the bitmap. Meanwhile, the new bitmap uses the variable "heightIndex" which starts a the top of the bitmap. When the old bitmap is moving up rows (with "--y" at the end of the for loop) the new bitmap is moving down rows (with "++heightIndex"). When this is done the image will be flipped right side up in the variable "*bitmapDone".

Technically you could stop there. But there is one small problem. When bitmap files are saved, each scanline must be padded to a DWORD (32bits). So if you have a bitmap that has a width not divisible by 4, than it will get messed up when you load it.

I'll get to how to fix that in my third "lesson" on this thread. If you have any more questions or didn't understand anything, PLEASE post a reply here on this post rather than e-mail me so everyone can see the question and my answer. I hope I was of some help. I gotta get back to programming my game. Have fun

[edited by - Big Sassy on June 20, 2002 12:23:58 AM]
Zackie62
Zackie62
Hi Big Sassy,
thanks for your great post.
It helps me a lot because I want
to load my bitmaps with the lovely
C File I/O Functions.

I would be very happy if you could
also describe,how to use the loaded bitmap
to create a direct draw surface with the
bitmap on it.

Thanks in advance,
Bye Zackie62
invective
invective
call me lazy, but why not just use the win32 API? You can load files with LoadImage, and use GDI to blt from the orginal color depth bit map to a DC with the target color format. The just save out the resulting header and bits and you have a converted bmp file.
Big Sassy
Big Sassy
quote:
Original post by invective
call me lazy, but why not just use the win32 API? You can load files with LoadImage, and use GDI to blt from the orginal color depth bit map to a DC with the target color format. The just save out the resulting header and bits and you have a converted bmp file.



You certainly can. But then you don''t understand how everything works. It''s really up to you (the programmer), but at the very least it''s good to understand what''s going on and then use the windows functions.


quote:
Zackie62
Hi Big Sassy,
thanks for your great post.



Your welcome I figured that these boards needed a clear and concise explanation on this topic. I think I did an ok job

quote:
Zackie62
I would be very happy if you could
also describe,how to use the loaded bitmap
to create a direct draw surface with the
bitmap on it.



I was hoping somebody would ask me that. I gotta do some things first, but I''ll post something later. Until then.
Big Sassy
Big Sassy
Now it's time to apply all we learned to DirectDraw surfaces. There's only a few different things we have to do. We have to create the surface, get the address of the surface's pixel data (which is where we'll store our bitmap), and account for it's pitch when we write the bitmap to it. I'm going to presume you already know how to create a surface and what goes into it. I'm also presuming that you're using DirectDraw 7.

Lets just say we have a DirectDraw object and a DirectDraw surface with a global scope. The declaration at the top of the file would look like this:


  
LPDIRECTDRAW7 directDraw;
LPDIRECTDRAWSURFACE7 bitSurface;


First thing we need to do is create the surface. We'll need a DirectDraw surface description to pass the DirectDraw object (so it knows what kind of surface it's building). It's kinda like handing the blueprint of your surface to DirectDraw and then it builds you a nice surface for you to work on. We'll define the blueprints like this:


  
// this will be declared with the other variables

DDSURFACEDESC2 surfaceDesc;

ZeroMemory(&surfaceDesc, sizeof(surfaceDesc));
surfaceDesc.dwSize = sizeof(surfaceDesc);
surfaceDesc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
surfaceDesc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
// here's where we specify the width and height we got from the bitmap file

surfaceDesc.dwWidth = infoheader.biWidth;
surfaceDesc.dwHeight = infoheader.biHeight;
// finally we pass on the "blueprint" to DirectDraw so it can build the surface

directDraw->CreateSurface(&surfaceDesc, &(bitSurface), NULL);


So now we got a nice surface to put our bitmap in. Unfortunately we don't know where DirectDraw created it's area in memory for the pixel data of the surface. Earlier we created our own area in memory when we dynamically allocated memory for each 16bit WORD pointer we made ("*bitmapData" and "*bitmapDone"). But now we didn't create the memory so we really don't know where this area in memory is for the surface.

Luckily we can get that information directly from the surface. To continue the blueprint analogy, if we pass it an empty blueprint it will fill it out with all the stuff that makes up the surface, including the memory address of it's pixel data storage. The code for that would look something like this:


  
// take the "blueprint" we used before and wipe it clean

ZeroMemory(&surfaceDesc, sizeof(surfaceDesc));
surfaceDesc.dwSize = sizeof(surfaceDesc);
// have the surface fill out the surface description

bitSurface->Lock(NULL, &surfaceDesc, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);


Now all we have to do is use a variable inside "surfaceDesc" called "lpSurface". It's a pointer to the area in memory. Just like "*bitmapData" and "*bitmapDone" do. Right now the finished bitmap goes to "*bitmapDone", and it points to an area in memory that we created. However, now we want the finished bitmap to go into "bitSurface"'s storage area for pixel data. So let's just have "*bitmapDone" point to the the same place "lpSurface" points to:


  
bitmapDone = (WORD *)surfaceDesc.lpSurface;


Since "bitmapDone" is a WORD pointer and "lpSurface" is a void pointer, we have to cast it to a WORD pointer. That's what the (WORD *) is all about. So now bitmapDone points to the surface's pixel data. We can finally load in all of our data like before. Except one thing. A DirectDraw surface isn't nesacarily the length you think it is. Let's look back at the for loop that goes through the entire bitmap:


  
int heightIndex = 0;
for( y=infoheader.biHeight-1; y>=0; --y)
{
for( x=0; x<infoheader.biWidth; ++x)
bitmapDone[heightIndex*infoheader.biWidth + x] = bitmapData[y*infoheader.biWidth + x];
++heightIndex;
}


Notice we used the incrementor from the first for loop (which would be "y") to move down each row. Remember that a bitmap isn't 2 deminsional in memory. It is just one straight line of memory. If we want to reach a certain row we would multiply the row number by the width of the bitmap. For example: If we had a 200x300 bitmap and we wanted to get to the 3rd row of the bitmap we would multiply the row (which is row 3) by the width of the bitmap (which is 200). This would give us 600. "bitmapData[600]" would equal the third row of the bitmap.

With DirectDraw surfaces we instead multiply the row number by the surface pitch. However, the surface pitch is the number of bytes in each row, rather than the number of pixels. Remember that a 16bit pixel has 2 bytes inside of it. Using the sample above, lets say a row is 200 pixels wide. Before (when we didn't multiply by the pitch) we would multiply by "y" by 200 to get the desired row we wanted. If we mulitplied the row we wanted by the surface's pitch would be multiplying it by 400 since there are 2 bytes in each pixel and 200 pixels in each row.

So before we multiply by the pitch to reach the next row we have to divide it by 2. Then we can multiply it by "y" (which is our incrementor for first for loop). That will take us to the proper row.

Maybe I didn't need to explain all of that, but hopefully you understand why we need to use the pitch. If not then don't worry about. Just know that instead of coding this:

bitmapDone[heightIndex*infoheader.biWidth + x] = bitmapData[y*infoheader.biWidth + x];

You would code this:

bitmapDone[heightIndex*(surfaceDesc.lPitch/2) + x] = bitmapData[y*infoheader.biWidth + x];

Now all we have to do is deallocate "*bitmapData", unlock the surface, and close the file and we're done. The whole function should now look like this:


  

// Global variables

LPDIRECTDRAW7 directDraw;
LPDIRECTDRAWSURFACE7 bitSurface;
// Macros

#define RGB16(red, green, blue) ( ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3))

void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
DDSURFACEDESC2 surfaceDesc;
WORD *bitmapData;
WORD *bitmapDone;
FILE *bitmapFile;
BYTE red, green, blue;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

ZeroMemory(&surfaceDesc, sizeof(surfaceDesc));
surfaceDesc.dwSize = sizeof(surfaceDesc);
surfaceDesc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
surfaceDesc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
surfaceDesc.dwWidth = infoheader.biWidth;
surfaceDesc.dwHeight = infoheader.biHeight;

directDraw->CreateSurface(&surfaceDesc, &(bitSurface), NULL);

ZeroMemory(&surfaceDesc, sizeof(surfaceDesc));
surfaceDesc.dwSize = sizeof(surfaceDesc);
bitSurface->Lock(NULL, &surfaceDesc, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];
bitmapDone = (WORD *)surfaceDesc.lpSurface;

for( int y=0; y<infoheader.biHeight; ++y)
{
for( int x=0; x<infoheader.biWidth; ++x)
{
fread(&blue, sizeof(BYTE), 1, file);
fread(&green, sizeof(BYTE), 1, file);
fread(&red, sizeof(BYTE), 1, file);
bitmapData[y*infoheader.biWidth + x] = RGB16(red, green, blue);
}
}

int heightIndex = 0;
for( y=infoheader.biHeight-1; y>=0; --y)
{
for( int x=0; x<infoheader.biWidth; ++x)
bitmapDone[heightIndex*surfaceDesc.lPitch + x] = bitmapData[y*infoheader.biWidth + x];
++heightIndex;
}

delete bitmapData;
bitSurface->Unlock(NULL);
fclose(file);
}


And that's it. Next I'll explain how to fix the padding problem.

[edited by - Big Sassy on May 23, 2002 11:14:17 PM]
Zackie62
Zackie62

Hi Big Sassy,
thanks a lot for your latest post.
Now I finally know how I can load bitmaps
out of my resource files!
*Zackie62 is so happy that he is jumping in the air*

Why don''t you write a tutorial(your posts in this thread are as good and as large as real tutorials) for gamedev.net about this topic.

I think a lot of people here including myself would really appreciate this!

Bye,
Zackie62
Big Sassy
Big Sassy
quote:
Original post by Zackie62

Hi Big Sassy,
thanks a lot for your latest post.
Now I finally know how I can load bitmaps
out of my resource files!
*Zackie62 is so happy that he is jumping in the air*

Why don''t you write a tutorial(your posts in this thread are as good and as large as real tutorials) for gamedev.net about this topic.

I think a lot of people here including myself would really appreciate this!

Bye,
Zackie62


Glad I could be of service I wouldn''t mine makng an article out of this. I''ll have to think about it. I''m going to make one last post on bitmap padding in a little while.

Mardigin
Mardigin
Big Sassy,

That is the best explanation I have read thus far. I sincerely appreciate your effort in helping out all us newbies. Please do post an article... Heck if you write a clear and consice tutorial book that covers graphics for game programmers email me and I will be the first to buy your book.

Thank you very much
Mardigin
Mardigin
Is it a better idea to write a converter that will convert from a 24 bit to a 16 bit bmp and save the new bmp so that you can load a real 16bit bmp directly? Or are there any advantages to simply doing all the conversion during load time in your game? The only advantage I can think of for not physically altering your original bmp is so you don''t loose any data. For instance perhaps in the future you would like to use that same bmp in a 32 bit game, but since you converted it to 16 bits you have lost data?

Is there a very large performance hit on having to convert during load time? Any opinions?
Big Sassy
Big Sassy
Well, for one that function can be optimized a lot. But usually you load either all your graphics in the beginning or load them when you reach a new level (or whatever). The time it takes to load each bitmap would not be acceptable if you were loading the bitmaps while the game was running. However, if you have a loading screen that lasts 5-40 seconds before the game starts then it isn't that big of a deal.

But making a coverter would save time during loading in the game. And since you'd be copying the original and not overwriting it, you could use the original later for stuff. It would be kind of a pain to convert all of the bitmaps, but you'd probably be able to see the difference in performance right away. I think it really comes down to how much you want to optimize. Many people wouldn't bother. But that is actually a good idea and it would speed up loading times. I don't know how much faster though. I'd say go for it just for the sake of experimentation.

I'm going to write up how to handle padding in the loading technique right now. It shouldn't take too long.

Edited by - Big Sassy on March 11, 2002 8:41:06 PM
Big Sassy
Big Sassy
Ok. There's one more thing that is weird about loading bitmaps. Each line of a bitmap is packed into DWORD's. A DWORD is 4 bytes long. When the amount of bytes in a line is not divisible by 4 then the amount of bytes that are needed to make it divisible by four are added to the end so it can fit in a DWORD. I know that sounds confusing (it was to me just yesterday) so let me break it down a bit.

Let's recap on how a 24bit bitmap is written to a file. Whenever a pixel in a 24bit bitmap is written to a file each of it's RGB values are saved to the file. So when the first pixel is saved, each part of it's RGB value is saved starting with blue, then green, and finally red (remember that they are written in backwards as BGR). Each of those values is 1 byte. So for each pixel 3 bytes are saved to the file. Let's just say we have a bitmap that is 7 pixels wide. Each line would look something like this. I had to use 2 lines since it stretched too far out on the page, so just remember that the other line is a continuation of the first line:

1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

11111111111111111111111111111111111111111111111111111111111111111111111111111111

There is 8bits for each blue, green, and red value for each pixel. Remember that 8bits equals 1byte. In bytes each line would look like this. I'm going to use orange and purple to seperate each pixel from now on. Each "1" represents a byte. There are 3 bytes in each pixel:

111111111111111111111

There are 3bytes (for each RGB value) for each pixel on each line. Now here is where the DWORD's come into play. Think about it like this. Before a pixel is written to the file each byte is put into a container. Each container can hold 4 bytes. When a container is filled, an empty container is brought out to be filled. This process continues until there are no more pixels for each line. Then it goes down to the next line and starts all over (and the next line gets a new container).

We'll continue with the example above with the 7 pixels for each line. The first pixel gets it's RGB values extracted, each being one byte. Now the blue value is put into the container. Now there's only 3 spots left in the container. We throw in the green value. Now there's only 2 spots. Now we throw in the red value. Now there's 1 spot.

Now the second pixel gets it's RGB values extracted. Once again, the blue value is put into the container. Now the container has all 4 spots taken and is full. So that is written to the file and a new container is brought out. So now we continue throwing in the second pixel's RGB value into the new container. The green value is put in. Now the container has 3 spots left. The red value is put in. Now the container has 2 spots left.

This continues until all the bytes in all the pixels is put into containers and written to a file. And this would be fine if every container was full when it was written to the file. But if there aren't enough pixels to fill the last container, all 4 spots are written to the file anyway. Let's look at how the top example would look like after it got written to the file. The top row is using alternating colors to seperate each pixel and the bottom row is using alternating color to seperate each container. Each "1" represents a byte:

111111111111111111111
111111111111111111111111

As you can see, the last container only has 1 byte of pixel data inside of it. The last 3 bytes at the end are just junk. That means each line of this bitmap will have 3 bytes of junk at the end of it. With the current function we have for loading bitmaps, we would load in that junk with the rest of the bitmap, messing everything up in the process. So how do we fix the problem? That's easy. We'll just skip over them.

We just need a formula to figure out how many we need to skip over at the end of each line. First we need to figure out how many bytes are on each line. Once again using the last example we know that each line has 7 pixels, and that each pixel contains 3 bytes. So if we take the amount of pixels on each line times 3 then we'll get the amount of bytes. So the formula would look like this so far:

pixel per line * 3 bytes = number of bytes per line

and with the example like this:

7 * 3 = 21

Now we need to figure out how many pixels will be left after the last container is filled all the way (not the last container in the line, just the last one with 4 spots filled). Since there are 4 bytes in each container, and all 21 of the pixels needs to be divided into a container, we'll divide all the pixels by 4. The remainder will be the amount of leftover bytes that are left. Luckily, C++ has an arithmetic operation called modulous that gets the remainder of a division (but you probably already knew that ). Now our formula looks like this:

(pixel per line * 3 bytes) % 4 = leftover bytes

and with the example like this:

(7 * 3) % 4 = leftover bytes
21 % 4 = 1

So now the last container will have 1 byte of data that we want. To find how many bytes are junk, we'll just subtract the size of the container (which is 4 bytes) by the leftover bytes. Now our formula looks like this:

4 - ((pixel per line * 3 bytes) % 4) = junk

and with the example like this:

4 - ((7 * 3) % 4) = junk
4 - (21 % 4) = junk
4 - 1 = 3

Simple, right? There's one minor problem. If the amount of pixels on each line is divisible by 4, then the remainder will be 0. The way our formula is now, we would take 4 - 0, which would equal 4. For example, let's say that each row is 8 pixels long rather than 7:

4 - ((8 * 3) % 4) = junk
4 - (24 % 4) = junk
4 - 0 = 4

Now our function will think there are 4 bytes of junk when there aren't any bytes of junk at all. So we'll make a simple if statement that will set it to 0 if the answer to the formula is equal to 4. This is what the source would look like that we are goint to add to our function:


  
// declare this variable with our other variables

int padding;
// get the padding at the end of the bitmap

padding = 4 - ((infoheader.biWidth * 3) % 4);
if(padding == 4)
padding = 0;


You'd think there'd be 15 lines of code after all of the explaining I did. I may have went a little overboard, but I just wanted to make sure you understood what the padding is all about. I don't really know why they decided to pack each line in DWORD's, but I think it has something to do with processor optimizations or something that makes passing DWORD's really fast. Don't quote me on that, though

Anyway, there's only one more step. When we reach the end of a line we are reading we gotta skip past the junk in the file. Thank god it's really simple, because I've typed enough these last few posts. The code would look like this in the for loop:


  
for( int y=0; y<infoheader.biHeight; ++y)
{
for( int x=0; x<infoheader.biWidth; ++x)
{
fread(&blue, sizeof(BYTE), 1, file);
fread(&green, sizeof(BYTE), 1, file);
fread(&red, sizeof(BYTE), 1, file);
bitmapData[y*infoheader.biWidth + x] = RGB16(red, green, blue);
}
// skip past the padding in the file

fseek(bitmapFile, padding, SEEK_CUR);
}


Now when we reach the end of the line we take padding (which is the amount of junk bytes at the end) and use fseek to skip ahead from the current point in the file (that's what "SEEK_CUR" is for) that many bytes.

And that's it. This is what the absolutely finished function looks like. It will take the DirectDraw surface "bitSurface" that was declared globally and load in the pixel data contained in the file "yourBitmap.bmp":


  
// Global variables

LPDIRECTDRAW7 directDraw;
LPDIRECTDRAWSURFACE7 bitSurface;

// Macros

#define RGB16(red, green, blue) ( ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3))


void LoadBitmap()
{
BITMAPINFOHEADER infoheader;
DDSURFACEDESC2 surfaceDesc;
WORD *bitmapData;
WORD *bitmapDone;
FILE *bitmapFile;
BYTE red, green, blue;
int padding;

bitmapFile = fopen("yourBitmap.bmp", "rb");
fseek(bitmapFile, sizeof(BITMAPFILEHEADER), SEEK_SET);
fread(&infoheader, sizeof(BITMAPINFOHEADER), 1, bitmapFile);

// get the padding at the end of the bitmap

padding = 4 - ((infoheader.biWidth * 3) % 4);
if(padding == 4)
padding = 0;

ZeroMemory(&surfaceDesc, sizeof(surfaceDesc));
surfaceDesc.dwSize = sizeof(surfaceDesc);
surfaceDesc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
surfaceDesc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
surfaceDesc.dwWidth = infoheader.biWidth;
surfaceDesc.dwHeight = infoheader.biHeight;

directDraw->CreateSurface(&surfaceDesc, &(bitSurface), NULL);

ZeroMemory(&surfaceDesc, sizeof(surfaceDesc));
surfaceDesc.dwSize = sizeof(surfaceDesc);
bitSurface->Lock(NULL, &surfaceDesc, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);

bitmapData = new WORD[infoheader.biWidth * infoheader.biHeight];
bitmapDone = (WORD *)surfaceDesc.lpSurface;

for( int y=0; y<infoheader.biHeight; ++y)
{
for( int x=0; x<infoheader.biWidth; ++x)
{
fread(&blue, sizeof(BYTE), 1, file);
fread(&green, sizeof(BYTE), 1, file);
fread(&red, sizeof(BYTE), 1, file);
bitmapData[y*infoheader.biWidth + x] = RGB16(red, green, blue);
}
// skip past the padding in the file

fseek(bitmapFile, padding, SEEK_CUR);
}

int heightIndex = 0;
for( y=infoheader.biHeight-1; y>=0; --y)
{
for( int x=0; x<infoheader.biWidth; ++x)
bitmapDone[heightIndex*(surfaceDesc.lPitch/2) + x] = bitmapData[y*infoheader.biWidth + x];

++heightIndex;
}

delete bitmapData;
bitSurface->Unlock(NULL);
fclose(file);
}


And that's it. Now you know what makes the bitmap file a bitmap file and how to read it in. In the process you've also learned a lot about file types in general and what makes them tick. I wouldn't be surprised if you got your own ideas on making your own image format just for your game. I made myself a sprite editor using Borland C++ Builder 5 that let's me edit animations of my sprites and saves them to my own format called *.ecs. An *.ecs file contains image data plus animation and transparency data (not transluceny). Mardigin had a great idea to convert bitmaps down to 16bit bitmaps so they can load faster.

As you can see, once you understand how file formats work they aren't that hard to work with. Now that you feel comfortable with the bitmap format, try playing around with some other file formats. www.wotsit.org has a crapload of formats you can peruse at your leisure. If you want a new challenge, try making a program that will read in a bitmap, let you draw on the surface (just plot pixels on the surface or blit graphics onto it) and save the surface data into a new bitmap. Once you can do that, you can consider yourself a master of the bitmap format

I decided I'm going to make an article on all of this since you guys want it. Hopefully Dave will accept it Now if you'll excuse me, I gotta get back to coding my own game. Once again if you have any questions, PLEASE post them here on this thread so everyone can see my answer.

[edited by - Big Sassy on May 23, 2002 11:15:53 PM]
Big Sassy
Big Sassy
Here is a tutorial I made on applying all I've said in these posts in a real program. A crappy program, but a real working program never the less.

quote:
Original post by AfTeRmAtH
Big Sassy, your just one sassy writer/typer arn't you :D

my game ][ my engine ]


Damn straight :)

EDIT - changed the link to the tutorial.

[Edited by - Big Sassy on May 8, 2007 6:11:31 AM]
aftermath
aftermath
wholly molly. When did the C++ code formating change. Ohh, ohh, wait... ... Was that you BigSassy ?

[ my engine ][ my game ][ my email ]
SPAM
Rate me up.
Big Sassy
Big Sassy
Dave changed the "source" tags yesterday. I personally like them

Edited by - Big Sassy on March 12, 2002 3:34:57 PM
Cybertron
Cybertron
Andre''s 16 bit macro is terrible. It uses a modulus, so a value from 0-31 is needed, and adds instead of ors! shifts fix that

use this:

#define _RGB16BIT565(r,g,b) ((b>>3) | ((g>>2) << 5) | ((r>>3) <<11))

I am not sure about the << 5 part, change it to 6 if it screws up
Jason Doucette
Jason Doucette
quote:
Original post by Cybertron
Andre''s 16 bit macro is terrible. It uses a modulus, so a value from 0-31 is needed, and adds instead of ors! shifts fix that

use this:

#define _RGB16BIT565(r,g,b) ((b>>3) | ((g>>2) << 5) | ((r>>3) <<11))

I am not sure about the << 5 part, change it to 6 if it screws up


Actually, I think the compiler will optimize this for you when you do a modulus for a number that is 2^n, n = integer. x mod n = x and (n-1). The shifts, like you said, are also way faster.

But, it''s still better to know why this would be faster (if the compiler didnt optimize it), and therefore you should use it yourself. This stuff is a must when converting code into assembly language - the compiler isn''t going to be optimizing anything when you start asking it to do divides when you could be ANDing or shifting.

Jason Doucette
www.jasondoucette.com

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.