Blitter Function, RAW Texture LoadingThis tutorial was originally written by Andreas Löffler. He also wrote all of the original HTML for the tutorial. A few days later Rob Fletcher emailed me an Irix version of lesson 29. In his version he rewrote most of the code. So I ported Rob's Irix / GLUT code to Visual C++ / Win32. I then modified the message loop code, and the fullscreen code. When the program is minimized it should use 0% of the CPU (or close to). When switching to and from fullscreen mode, most of the problems should be gone (screen not restoring properly, messed up display, etc). Andreas tutorial is now better than ever. Unfortunately, the code has been modifed quite a bit, so all of the HTML has been rewritten by myself. Huge Thanks to Andreas for getting the ball rolling, and working his butt off to make a killer tutorial. Thanks to Rob for the modifications! Lets begin... We create a device mode structure called DMsaved. We will use this structure to store information about the users default desktop resolution, color depth, etc., before we switch to fullscreen mode. More on this later! Notice we only allocate enough storage space for one texture (texture[1]).
Now for the fun stuff. We create a structure called TEXTURE_IMAGE. The structure contains information about our images width, height, and format (bytes per pixel). data is a pointer to unsigned char. Later on data will point to our image data.
We then create a pointer called P_TEXTURE_IMAGE to the TEXTURE_IMAGE data type. The variables t1 and t2 are of type P_TEXTURE_IMAGE where P_TEXTURE_IMAGE is a redefined type of pointer to TEXTURE_IMAGE.
Below is the code to allocate memory for a texture. When we call this code, we pass it the width, height and bytes per pixel information of the image we plan to load. ti is a pointer to our TEXTURE_IMAGE data type. It's given a NULL value. c is a pointer to unsigned char, it is also set to NULL.
Here is where we allocate the memory for our image structure. If everything goes well, ti will point to the allocated memory. After allocating the memory, and checking to make sure ti is not equal to NULL, we can fill the structure with the image attributes. First we set the width (w), then the height (h) and lastly the format (f). Keep in mind format is bytes per pixel.
Now we need to allocate memory for the actual image data. The calculation is easy! We multiply the width of the image (w) by the height of the image (h) then multiply by the format (f - bytes per pixel).
We check to see if everything went ok. If the value in c is not equal to NULL we set the data variable in our structure to point to the newly allocated memory. If there was a problem, we pop up an error message on the screen letting the user know that the program was unable to allocate memory for the texture buffer. NULL is returned.
If anything went wrong when we were trying to allocate memory for our image structure, the code below would pop up an error message and return NULL. If there were no problems, we return ti which is a pointer to our newly allocated image structure. Whew... Hope that all made sense.
When it comes time to release the memory, the code below will deallocate the texture buffer and then free the image structure. t is a pointer to the TEXTURE_IMAGE data structure we want to deallocate.
Now we read in our .RAW image. We pass the filename and a pointer to the image structure we want to load the image into. We set up our misc variables, and then calculate the size of a row. We figure out the size of a row by multiplying the width of our image by the format (bytes per pixel). So if the image was 256 pixels wide and there were 4 bytes per pixel, the width of a row would be 1024 bytes. We store the width of a row in stride. We set up a pointer (p), and then attempt to open the file.
If the file exists, we set up the loops to read in our texture. i starts at the bottom of the image and moves up a line at a time. We start at the bottom so that the image is flipped the right way. .RAW images are stored upside down. We have to set our pointer now so that the data is loaded into the proper spot in the image buffer. Each time we move up a line (i is decreased) we set the pointer to the start of the new line. data is where our image buffer starts, and to move an entire line at a time in the buffer, multiply i by stride. Remember that stride is the length of a line in bytes, and i is the current line. So by multiplying the two, we move an entire line at a time. The j loop moves from left (0) to right (width of line in pixels, not bytes).
The k loop reads in our bytes per pixel. So if format (bytes per pixel) is 4, k loops from 0 to 2 which is bytes per pixel minus one (format-1). The reason we subtract one is because most raw images don't have an alpha value. We want to make the 4th byte our alpha value, and we want to set the alpha value manually. Notice in the loop we also increase the pointer (p) and a variable called done. More about done later. The line inside the loop reads a character from our file and stores it in the texture buffer at our current pointer location. If our image has 4 bytes per pixel, the first 3 bytes will be read from the .RAW file (format-1), and the 4th byte will be manually set to 255. After we set the 4th byte to 255 we increase the pointer location by one so that our 4th byte is not overwritten with the next byte in the file. After all of the bytes have been read in per pixel, and all of the pixels have been read in per row, and all of the rows have been read in, we are done! We can close the file.
If there was a problem opening the file (does not exist, etc), the code below will pop up a message box letting the user know that the file could not be opened. The last thing we do is return done. If the file couldn't be opened, done will equal 0. If everything went ok, done should equal the number of bytes read from the file. Remember, we were increasing done every time we read a byte in the loop above (k loop).
This shouldn't need explaining. By now you should know how to build a texture. tex is the pointer to the TEXTURE_IMAGE structure that we want to use. We build a linear filtered texture. In this example, we're building mipmaps (smoother looking). We pass the width, height and data just like we would if we were using glaux, but this time we get the information from the selected TEXTURE_IMAGE structure.
Now for the blitter code :) The blitter code is very powerful. It lets you copy any section of a (src) texture and paste it into a destination (dst) texture. You can combine as many textures as you want, you can set the alpha value used for blending, and you can select whether the two images blend together or cancel eachother out. src is the TEXTURE_IMAGE structure to use as the source image. dst is the TEXTURE_IMAGE structure to use for the destination image. src_xstart is where you want to start copying from on the x axis of the source image. src_ystart is where you want to start copying from on the y axis of the source image. src_width is the width in pixels of the area you want to copy from the source image. src_height is the height in pixels of the area you want to copy from the source image. dst_xstart and dst_ystart is where you want to place the copied pixels from the source image onto the destination image. If blend is 1, the two images will be blended. alpha sets how transparent the copied image will be when it mapped onto the destination image. 0 is completely clear, and 255 is solid. We set up all our misc loop variables, along with pointers for our source image (s) and destination image (d). We check to see if the alpha value is within range. If not, we clamp it. We do the same for the blend value. If it's not 0-off or 1-on, we clamp it.
Now we have to set up the pointers. The destination pointer is the location of the destination data plus the starting location on the destination images y axis (dst_ystart) * the destination images width in pixels * the destination images bytes per pixel (format). This should give us the starting row for our destination image. We do pretty much the same thing for the source pointer. The source pointer is the location of the source data plus the starting location on the source images y axis (src_ystart) * the source images width in pixels * the source images bytes per pixel (format). This should give us the starting row for our source image. i loops from 0 to src_height which is the number of pixels to copy up and down from the source image.
We already set the source and destination pointers to the correct rows in each image. Now we have to move to the correct location from left to right in each image before we can start blitting the data. We increase the location of the source pointer (s) by src_xstart which is the starting location on the x axis of the source image times the source images bytes per pixel. This moves the source (s) pointer to the starting pixel location on the x axis (from left to right) on the source image. We do the exact same thing for the destination pointer. We increase the location of the destination pointer (d) by dst_xstart which is the starting location on the x axis of the destination image multiplied by the destination images bytes per pixel (format). This moves the destination (d) pointer to the starting pixel location on the x axis (from left to right) on the destination image. After we have calculated where in memory we want to grab our pixels from (s) and where we want to move them to (d), we start the j loop. We'll use the j loop to travel from left to right through the source image.
The k loop is used to go through all the bytes per pixel. Notice as k increases, our pointers for the source and destination images also increase. Inside the loop we check to see if blending is on or off. If blend is 1, meaning we should blend, we do some fancy math to calculate the color of our blended pixels. The destination value (d) will equal our source value (s) multiplied by our alpha value + our current destination value (d) times 255 minus the alpha value. The shift operator (>>8) keeps the value in a 0-255 range. If blending is disabled (0), we copy the data from the source image directly into the destination image. No blending is done and the alpha value is ignored.
The InitGL() code has changed quite a bit. All of the code below is new. We start off by allocating enough memory to hold a 256x256x4 Bytes Per Pixel Image. t1 will point to the allocated ram if everything went well. After allocating memory for our image, we attempt to load the image. We pass ReadTextureData() the name of the file we wish to open, along with a pointer to our Image Structure (t1). If we were unable to load the .RAW image, a message box will pop up on the screen to let the user know there was a problem loading the texture. We then do the same thing for t2. We allocate memory, and attempt to read in our second .RAW image. If anything goes wrong we pop up a message box.
If we got this far, it's safe to assume the memory has been allocated and the images have been loaded. Now to use our Blit() command to merge the two images into one. We start off by passing Blit() t2 and t1, both point to our TEXTURE_IMAGE structures (t2 is the second image, t1 is the first image). Then we have to tell blit where to start grabbing data from on the source image. If you load the source image into Adobe Photoshop or any other program capable of loading .RAW images you will see that the entire image is blank except for the top right corner. The top right has a picture of the ball with GL written on it. The bottom left corner of the image is 0,0. The top right of the image is the width of the image-1 (255), the height of the image-1 (255). Knowing that we only want to copy 1/4 of the src image (top right), we tell Blit() to start grabbing from 127,127 (center of our source image). Next we tell blit how many pixels we want to copy from our source point to the right, and from our source point up. We want to grab a 1/4 chunk of our image. Our image is 256x256 pixels, 1/4 of that is 128x128 pixels. All of the source information is done. Blit() now knows that it should copy from 127 on the x axis to 127+128 (255) on the x axis, and from 127 on the y axis to 127+128 (255) on the y axis. So Blit() knows what to copy, and where to get the data from, but it doesn't know where to put the data once it's gotten it. We want to draw the ball with GL written on it in the middle of the monitor image. You find the center of the destination image (256x256) which is 128x128 and subtract half the width and height of the source image (128x128) which is 64x64. So (128-64) x (128-64) gives us a starting location of 64,64. Last thing to do is tell our blitter routine we want to blend the two images (A one means blend, a zero means do not blend), and how much to blend the images. If the last value is 0, we blend the images 0%, meaning anything we copy will replace what was already there. If we use a value of 127, the two images blend together at 50%, and if you use 255, the image you are copying will be completely transparent and will not show up at all. The pixels are copied from image2 (t2) to image1 (t1). The mixed image will be stored in t1.
After we have mixed the two images (t1 and t2) together, we build a texture from the combined images (t1). After the texture has been created, we can deallocate the memory holding our two TEXTURE_IMAGE structures. The rest of the code is pretty standard. We enable texture mapping, depth testing, etc.
I shouldn't even have to explain the code below. We move 5 units into the screen, select our single texture, and draw a texture mapped cube. You should notice that both textures are now combined into one. We don't have to render everything twice to map both textures onto the cube. The blitter code combined the images for us.
The KillGLWindow() code has a few changes. You'll notice the code to switch from fullscreen mode back to your desktop is now at the top of KillGLWindow(). If the user ran the program in fullscreen mode, the first thing we do when we kill the window is try to switch back to the desktop resolution. If the quick way fails to work, we reset the screen using the information stored in DMsaved. This should restore us to our orignal desktop settings.
I've made some changes in CreateGLWindow. The changes will hopefully elimintate alot of the problems people are having when they switch to and from from fullscreen mode. I've included the first part of CreateGLWindow() so you can easily follow through the code.
The big change here is that we now save the current desktop resolution, bit depth, etc. before we switch to fullscreen mode. That way when we exit the program, we can set everything back exactly how it was. The first line below copies the display settings into the DMsaved Device Mode structure. Nothing else has changed, just one new line of code.
WinMain() starts out the same as always. Ask the user if they want fullscreen or not, then start the loop.
I have made some changes to the code below. If the program is not active (minimized) we wait for a message with the command WaitMessage(). Everything stops until the program receives a message (usually maximizing the window). What this means is that the program no longer hogs the processor while it's minimized. Thanks to Jim Strong for the suggestion.
Well, that´s it! Now the doors are open for creating some very cool blending effects for your games, engines or even applications. With texture buffers we used in this tutorial you could do more cool effects like real-time plasma or water. When combining these effects all together you´re able to do nearly photo-realistic terrain. If something doesn´t work in this tutorial or you have suggestions how to do it better, then please don´t hesitate to E-Mail me. Thank you for reading and good luck in creating your own special effects! Some information about Andreas: I´m an 18 years old pupil who is currently studying to be a software engineer. I´ve been programming for nearly 10 years now. I've been programming in OpenGL for about 1.5 years. Andreas Löffler & Rob Fletcher Jeff Molofee (NeHe) * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Christian Kindahl )
|