MaskingWelcome to Tutorial 20. The bitmap image format is supported on just about every computer, and just about every operating system. Not only is it easy to work with, it's very easy to load and use as a texture. Up until now, we've been using blending to place text and other images onto the screen without erasing what's underneath the text or image. This is effective, but the results are not always pretty. Most the time a blended texture blends in too much or not enough. When making a game using sprites, you don't want the scene behind your character shining through the characters body. When writing text to the screen you want the text to be solid and easy to read. That's where masking comes in handy. Masking is a two step process. First we place a black and white image of our texture on top of the scene. The white represents the transparent part of our texture. The black represents the solid part of our texture. Because of the type of blending we use, only the black will appear on the scene. Almost like a cookie cutter effect. Then we switch blending modes, and map our texture on top of the black cut out. Again, because of the blending mode we use, the only parts of our texture that will be copied to the screen are the parts that land on top of the black mask. I'll rewrite the entire program in this tutorial aside from the sections that haven't changed. So if you're ready to learn something new, let's begin! #include <windows.h> // Header File For Windows #include <math.h> // Header File For Windows Math Library #include <stdio.h> // Header File For Standard Input/Output #include <gl\gl.h> // Header File For The OpenGL32 Library #include <gl\glu.h> // Header File For The GLu32 Library #include <gl\glaux.h> // Header File For The Glaux Library HDC hDC=NULL; // Private GDI Device Context HGLRC hRC=NULL; // Permanent Rendering Context HWND hWnd=NULL; // Holds Our Window Handle HINSTANCE hInstance; // Holds The Instance Of The Application We'll be using 7 global variables in this program. masking is a boolean variable (TRUE / FALSE) that will keep track of whether or not masking is turned on of off. mp is used to make sure that the 'M' key isn't being held down. sp is used to make sure that the 'Spacebar' isn't being held down and the variable scene will keep track of whether or not we're drawing the first or second scene. We set up storage space for 5 textures using the variable texture[5]. loop is our generic counter variable, we'll use it a few times in our program to set up textures, etc. Finally we have the variable roll. We'll use roll to roll the textures across the screen. Creates a neat effect! We'll also use it to spin the object in scene 2. bool keys[256]; // Array Used For The Keyboard Routine bool active=TRUE; // Window Active Flag Set To TRUE By Default bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default bool masking=TRUE; // Masking On/Off bool mp; // M Pressed? bool sp; // Space Pressed? bool scene; // Which Scene To Draw GLuint texture[5]; // Storage For Our Five Textures GLuint loop; // Generic Loop Variable GLfloat roll; // Rolling Texture LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc The load bitmap code hasn't changed. It's the same as it was in lesson 6, etc. In the code below we create storage space for 5 images. We clear the space and load in all 5 bitmaps. We loop through each image and convert it into a texture for use in our program. The textures are stored in texture[0-4]. int LoadGLTextures() // Load Bitmaps And Convert To Textures { int Status=FALSE; // Status Indicator AUX_RGBImageRec *TextureImage[5]; // Create Storage Space For The Texture Data memset(TextureImage,0,sizeof(void *)*5); // Set The Pointer To NULL if ((TextureImage[0]=LoadBMP("Data/logo.bmp")) && // Logo Texture (TextureImage[1]=LoadBMP("Data/mask1.bmp")) && // First Mask (TextureImage[2]=LoadBMP("Data/image1.bmp")) && // First Image (TextureImage[3]=LoadBMP("Data/mask2.bmp")) && // Second Mask (TextureImage[4]=LoadBMP("Data/image2.bmp"))) // Second Image { Status=TRUE; // Set The Status To TRUE glGenTextures(5, &texture[0]); // Create Five Textures for (loop=0; loop<5; loop++) // Loop Through All 5 Textures { glBindTexture(GL_TEXTURE_2D, texture[loop]); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX, TextureImage[loop]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data); } } for (loop=0; loop<5; loop++) // Loop Through All 5 Textures { if (TextureImage[loop]) // If Texture Exists { if (TextureImage[loop]->data) // If Texture Image Exists { free(TextureImage[loop]->data); // Free The Texture Image Memory } free(TextureImage[loop]); // Free The Image Structure } } return Status; // Return The Status } The ReSizeGLScene() code hasn't changed so we'll skip over it. The Init code is fairly bare bones. We load in our textures, set the clear color, set and enable depth testing, turn on smooth shading, and enable texture mapping. Simple program so no need for a complex init :) int InitGL(GLvoid) // All Setup For OpenGL Goes Here { if (!LoadGLTextures()) // Jump To Texture Loading Routine { return FALSE; // If Texture Didn't Load Return FALSE } glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Clear The Background Color To Black glClearDepth(1.0); // Enables Clearing Of The Depth Buffer glEnable(GL_DEPTH_TEST); // Enable Depth Testing glShadeModel(GL_SMOOTH); // Enables Smooth Color Shading glEnable(GL_TEXTURE_2D); // Enable 2D Texture Mapping return TRUE; // Initialization Went OK } Now for the fun stuff. Our drawing code! We start off the same as usual. We clear the background color and the depth buffer. Then we reset the modelview matrix, and translate into the screen 2 units so that we can see our scene. int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer glLoadIdentity(); // Reset The Modelview Matrix glTranslatef(0.0f,0.0f,-2.0f); // Move Into The Screen 5 Units The first line below selects the 'logo' texture. We'll map the texture to the screen using a quad. We specify our four texture coordinates along with our four vertices. Revised Description by Jonathan Roy: Remember that OpenGL is a vertex-based graphic system. Most of the parameters you set are recorded as attributes of a particular vertex. Texture coordinate is one such attribute. You simply specify appropriate texture coordinates for each vertex of a polygon, and OpenGL automatically fills in the surface between the vertices with the texture, through a process known as interpolation. Interpolation is a standard geometric technique that lets OpenGL determine how a given parameter varies between vertices just by knowing the value that parameter takes at the vertices themselves. Like in the previous lessons, we pretend we are facing the quad and assign texture coordinates as follows: (0.0, 0.0) to the bottom-left corner, (0.0, 1.0) to the top-left corner, (1.0, 0.0) to the bottom-right, and (1.0, 1.0) to the top-right. Now, given these settings, can you tell what texture coordinates correspond to the quad's middle point? That's right, (0.5, 0.5). But no where in the code did you specify that coordinate, did you? When it draws the quad, OpenGL computes it for you. And the real magic is that it does so whatever the position, size, or orientation of the polygon! In this lesson we add another interesting twist by assigning texture coordinates with values other than 0.0 and 1.0. Texture coordinates are said to be normalized. Value 0.0 maps to one edge of the texture, while value 1.0 maps to the opposite edge, spanning the full width or height of the texture image in a one unit step, regardless of the polygon's size or the image's size in pixels (which we therefore don't have to worry about when doing texture mapping, and that makes life a whole lot easier). Above 1.0, the mapping simply wraps around at the other edge and the texture repeats. In other words, texture coordinate (0.3, 0.5) for instance, maps to the exact same pixel in the texture image as coordinate (1.3, 0.5), or as (12.3, -2.5). In this lesson, we achieve a tiling effect by specifying value 3.0 instead of 1.0, effectively repeating the texture nine times (3x3 tiling) over the surface of the quad. Additionally, we use the roll variable to translate (or slide) the texture over the surface of the quad. A value of 0.0 for roll, which is added to the vertical texture coordinate, means that texture mapping on the bottom edge of the quad begins at the bottom edge of the texture image, as shown in the figure on the left. When roll equals 0.5, mapping on the bottom edge of the quad begins halfway up in the image (see figure on the right). Rolling textures can be used to create great effects such as moving clouds, words spinning around an object, etc. glBindTexture(GL_TEXTURE_2D, texture[0]); // Select Our Logo Texture glBegin(GL_QUADS); // Start Drawing A Textured Quad glTexCoord2f(0.0f, -roll+0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Bottom Left glTexCoord2f(3.0f, -roll+0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Bottom Right glTexCoord2f(3.0f, -roll+3.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Top Right glTexCoord2f(0.0f, -roll+3.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Top Left glEnd(); // Done Drawing The Quad Anyways... back to reality. Now we enable blending. In order for this effect to work we also have to disable depth testing. It's very important that you do this! If you do not disable depth testing you probably wont see anything. Your entire image will vanish! glEnable(GL_BLEND); // Enable Blending glDisable(GL_DEPTH_TEST); // Disable Depth Testing The first thing we do after we enable blending and disable depth testing is check to see if we're going to mask our image or blend it the old fashioned way. The line of code below checks to see if masking is TRUE. If it is we'll set up blending so that our mask gets drawn to the screen properly. if (masking) // Is Masking Enabled? { If masking is TRUE the line below will set up blending for our mask. A mask is just a copy of the texture we want to draw to the screen but in black and white. Any section of the mask that is white will be transparent. Any sections of the mask that is black will be SOLID. The blend command below does the following: The Destination color (screen color) will be set to black if the section of our mask that is being copied to the screen is black. This means that sections of the screen that the black portion of our mask covers will turn black. Anything that was on the screen under the mask will be cleared to black. The section of the screen covered by the white mask will not change. glBlendFunc(GL_DST_COLOR,GL_ZERO); // Blend Screen Color With Zero (Black) } Now we check to see what scene to draw. If scene is TRUE we will draw the second scene. If scene is FALSE we will draw the first scene. if (scene) // Are We Drawing The Second Scene? { We don't want things to be too big so we translate one more unit into the screen. This reduces the size of our objects. After we translate into the screen, we rotate from 0-360 degrees depending on the value of roll. If roll is 0.0 we will be rotating 0 degrees. If roll is 1.0 we will be rotating 360 degrees. Fairly fast rotation, but I didn't feel like creating another variable just to rotate the image in the center of the screen. :) glTranslatef(0.0f,0.0f,-1.0f); // Translate Into The Screen One Unit glRotatef(roll*360,0.0f,0.0f,1.0f); // Rotate On The Z Axis 360 Degrees We already have the rolling logo on the screen and we've rotated the scene on the Z axis causing any objects we draw to be rotated counter-clockwise, now all we have to do is check to see if masking is on. If it is we'll draw our mask then our object. If masking is off we'll just draw our object. if (masking) // Is Masking On? { If masking is TRUE the code below will draw our mask to the screen. Our blend mode should be set up properly because we had checked for masking once already while setting up the blending. Now all we have to do is draw the mask to the screen. We select mask 2 (because this is the second scene). After we have selected the mask texture we texture map it onto a quad. The quad is 1.1 units to the left and right so that it fills the screen up a little more. We only want one texture to show up so our texture coordinates only go from 0.0 to 1.0. After drawing our mask to the screen a solid black copy of our final texture will appear on the screen. The final result will look as if someone took a cookie cutter and cut the shape of our final texture out of the screen, leaving an empty black space. glBindTexture(GL_TEXTURE_2D, texture[3]); // Select The Second Mask Texture glBegin(GL_QUADS); // Start Drawing A Textured Quad glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Bottom Left glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Bottom Right glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Top Right glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Top Left glEnd(); // Done Drawing The Quad } Now that we have drawn our mask to the screen it's time to change blending modes again. This time we're going to tell OpenGL to copy any part of our colored texture that is NOT black to the screen. Because the final texture is an exact copy of the mask but with color, the only parts of our texture that get drawn to the screen are parts that land on top of the black portion of the mask. Because the mask is black, nothing from the screen will shine through our texture. This leaves us with a very solid looking texture floating on top of the screen. Notice that we select the second image after selecting the final blending mode. This selects our colored image (the image that our second mask is based on). Also notice that we draw this image right on top of the mask. Same texture coordinates, same vertices. If we don't lay down a mask, our image will still be copied to the screen, but it will blend with whatever was on the screen. glBlendFunc(GL_ONE, GL_ONE); // Copy Image 2 Color To The Screen glBindTexture(GL_TEXTURE_2D, texture[4]); // Select The Second Image Texture glBegin(GL_QUADS); // Start Drawing A Textured Quad glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Bottom Left glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Bottom Right glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Top Right glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Top Left glEnd(); // Done Drawing The Quad } If scene was FALSE, we will draw the first scene (my favorite). else // Otherwise { We start off by checking to see if masking is TRUE of FALSE, just like in the code above. if (masking) // Is Masking On? { If masking is TRUE we draw our mask 1 to the screen (the mask for scene 1). Notice that the texture is rolling from right to left (roll is added to the horizontal texture coordinate). We want this texture to fill the entire screen that is why we never translated further into the screen. glBindTexture(GL_TEXTURE_2D, texture[1]); // Select The First Mask Texture glBegin(GL_QUADS); // Start Drawing A Textured Quad glTexCoord2f(roll+0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Bottom Left glTexCoord2f(roll+4.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Bottom Right glTexCoord2f(roll+4.0f, 4.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Top Right glTexCoord2f(roll+0.0f, 4.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Top Left glEnd(); // Done Drawing The Quad } Again we enable blending and select our texture for scene 1. We map this texture on top of it's mask. Notice we roll this texture as well, otherwise the mask and final image wouldn't line up. glBlendFunc(GL_ONE, GL_ONE); // Copy Image 1 Color To The Screen glBindTexture(GL_TEXTURE_2D, texture[2]); // Select The First Image Texture glBegin(GL_QUADS); // Start Drawing A Textured Quad glTexCoord2f(roll+0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Bottom Left glTexCoord2f(roll+4.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Bottom Right glTexCoord2f(roll+4.0f, 4.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Top Right glTexCoord2f(roll+0.0f, 4.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Top Left glEnd(); // Done Drawing The Quad } Next we enable depth testing, and disable blending. This prevents strange things from happening in the rest of our program :) glEnable(GL_DEPTH_TEST); // Enable Depth Testing glDisable(GL_BLEND); // Disable Blending Finally all we have left to do is increase the value of roll. If roll is greater than 1.0 we subtract 1.0. This prevents the value of roll from getting to high. roll+=0.002f; // Increase Our Texture Roll Variable if (roll>1.0f) // Is Roll Greater Than One { roll-=1.0f; // Subtract 1 From Roll } return TRUE; // Everything Went OK } The KillGLWindow(), CreateGLWindow() and WndProc() code hasn't changed so we'll skip over it. The first thing you will notice different in the WinMain() code is the Window title. It's now titled "NeHe's Masking Tutorial". Change it to whatever you want :) int WINAPI WinMain( HINSTANCE hInstance, // Instance HINSTANCE hPrevInstance, // Previous Instance LPSTR lpCmdLine, // Command Line Parameters int nCmdShow) // Window Show State { MSG msg; // Windows Message Structure BOOL done=FALSE; // Bool Variable To Exit Loop // Ask The User Which Screen Mode They Prefer if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO) { fullscreen=FALSE; // Windowed Mode } // Create Our OpenGL Window if (!CreateGLWindow("NeHe's Masking Tutorial",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created } while(!done) // Loop That Runs While done=FALSE { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting? { if (msg.message==WM_QUIT) // Have We Received A Quit Message? { done=TRUE; // If So done=TRUE } else // If Not, Deal With Window Messages { TranslateMessage(&msg); // Translate The Message DispatchMessage(&msg); // Dispatch The Message } } else // If There Are No Messages { // Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene() if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Active? Was There A Quit Received? { done=TRUE; // ESC or DrawGLScene Signalled A Quit } else // Not Time To Quit, Update Screen { SwapBuffers(hDC); // Swap Buffers (Double Buffering) Now for our simple key handling code. We check to see if the spacebar is being pressed. If it is, we set the sp variable to TRUE. If sp is TRUE, the code below will not run a second time until the spacebar has been released. This keeps our program from flipping back and forth from scene to scene very rapidly. After we set sp to TRUE, we toggle the scene. If it was TRUE, it becomes FALSE, if it was FALSE it becomes TRUE. In our drawing code above, if scene is FALSE the first scene is drawn. If scene is TRUE the second scene is drawn. if (keys[' '] && !sp) // Is Space Being Pressed? { sp=TRUE; // Tell Program Spacebar Is Being Held scene=!scene; // Toggle From One Scene To The Other } The code below checks to see if we have released the spacebar (if NOT ' '). If the spacebar has been released, we set sp to FALSE letting our program know that the spacebar is NOT being held down. By setting sp to FALSE the code above will check to see if the spacebar has been pressed again, and if so the cycle will start over. if (!keys[' ']) // Has Spacebar Been Released? { sp=FALSE; // Tell Program Spacebar Has Been Released } The next section of code checks to see if the 'M' key is being pressed. If it is being pressed, we set mp to TRUE, telling our program not to check again until the key is released, and we toggle masking from TRUE to FALSE or FALSE to TRUE. If masking is TRUE, the drawing code will turn on masking. If it is FALSE masking will be off. If masking is off, the object will be blended to the screen using the old fashioned blending we've been using up until now. if (keys['M'] && !mp) // Is M Being Pressed? { mp=TRUE; // Tell Program M Is Being Held masking=!masking; // Toggle Masking Mode OFF/ON } The last bit of code checks to see if we've stopped pressing 'M'. If we have, mp becomes FALSE letting the program know that we are no longer holding the 'M' key down. Once the 'M' key has been released, we are able to press it once again to toggle masking on or off. if (!keys['M']) // Has M Been Released? { mp=FALSE; // Tell Program That M Has Been Released } Like all the previous tutorials, make sure the title at the top of the window is correct. if (keys[VK_F1]) // Is F1 Being Pressed? { keys[VK_F1]=FALSE; // If So Make Key FALSE KillGLWindow(); // Kill Our Current Window fullscreen=!fullscreen; // Toggle Fullscreen / Windowed Mode // Recreate Our OpenGL Window if (!CreateGLWindow("NeHe's Masking Tutorial",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created } } } } } // Shutdown KillGLWindow(); // Kill The Window return (msg.wParam); // Exit The Program } Creating a mask isn't to hard. A little time consuming. The best way to make a mask if you already have your image made is to load your image into an art program or a handy program like infranview, and reduce it to a gray scale image. After you've done that, turn the contrast way up so that gray pixels become black. You can also try turning down the brightness, etc. It's important that the white is bright white, and the black is pure black. If you have any gray pixels in your mask, that section of the image will appear transparent. The most reliable way to make sure your mask is a perfect copy of your image is to trace over the image with black. It's also very important that your image has a BLACK background and the mask has a WHITE background! If you create a mask and notice a square shape around your texture, either your white isn't bright enough (255 or FFFFFF) or your black isn't true black (0 or 000000). Below you can see an example of a mask and the image that goes over top of the mask. The image can be any color you want as long as the background is black. The mask must have a white background and a black copy of your image.
Eric Desrosiers pointed out that you can also check the value of each pixel in your bitmap while you load it. If you want the pixel transparent you can give it an alpha value of 0. For all the other colors you can give them an alpha value of 255. This method will also work but requires some extra coding. The current tutorial is simple and requires very little extra code. I'm not blind to other techniques, but when I write a tutorial I try to make the code easy to understand and easy to use. I just wanted to point out that there are always other ways to get the job done. Thanks for the feedback Eric. In this tutorial I have shown you a simple, but effective way to draw sections of a texture to the screen without using the alpha channel. Normal blending usually looks bad (textures are either transparent or they're not), and texturing with an alpha channel requires that your images support the alpha channel. Bitmaps are convenient to work with, but they do not support the alpha channel this program shows us how to get around the limitations of bitmap images, while demonstrating a cool way to create overlay type effects. Thanks to Rob Santa for the idea and for example code. I had never heard of this little trick until he pointed it out. He wanted me to point out that although this trick does work, it takes two passes, which causes a performance hit. He recommends that you use textures that support the alpha channel for complex scenes. I hope you enjoyed this tutorial. If you had any problems understanding it, or you've found a mistake in the tutorial please let me know. I want to make the best tutorials available. Your feedback is important! Jeff Molofee (NeHe) * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Christian Kindahl ) |