ELEMENTOS CONFIGURADORES DEL RITMO AUDIOVISUAL
Capítulo 6. El guion
6.3. La importancia de la premisa
Clipping & Reflections Using The Stencil
Buffer Buffer
Welcome to another exciting tutorial. The code for this tutorial was written by Banu Cosmin. The tutorial was of course written by myself (NeHe). In this tutorial you will learn how to create EXTREMELY realistic reflections. Nothing fake here! The objects being reflected will not show up underneath the floor or on the other side of a wall. True reflections!
A very important thing to note about this tutorial: Because the Voodoo 1, 2 and some other cards do not support the stencil buffer, this demo will NOT run on those cards. It will ONLY run on cards that support the stencil buffer. If you're not sure if your card supports the stencil buffer, download the code, and try running the demo. Also, this demo requires a fai rly decent processor and graphics card. Even on my GeForce I notice there is a little slow down at times. This demo runs best in 32 bit color mode!
As video cards get better, and processors get faster, I can see the stencil buffer becoming more popular. If you have the hardware and you're ready to reflect, read on!
The first part of the code is fairly standard. W e include all necessary header files, and set up our Device Context, Rendering Context, etc.
#include <windows.h>// Header File For Windows
#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
#include <stdio.h>// Header File For Standard Input / Output HDC hDC=NULL;// Private GDI Device Context
HGLRC hRC=NULL; // Permanent Rendering Context HWND hWnd=NULL; // Holds Our Window Handle
HINSTANCE hInstance = NULL;// Holds The Instance Of The Application
Next we have the standard variables to keep track of key presses (keys[ ]), whether or not the program is active (active), and if we should use fullscreen mode or windowed mode (f ullscreen).
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
Next we set up our lighting variables. LightAmb[ ] will set our ambient light. We will use 70% red, 70% green and 70% blue, creating a light that is 70% bright white. LightDif[ ] will set the diffuse lighting (the amount of light evenly reflected off the surface of our object). In this case we want to reflect full intensity light. Lastly we have LightPos[ ] which will be used to position our light. In this case we want the light 4 units to the right, 4 units up, and 6 units towards the viewer. If we could actually see the light, it would be floating in front of the top right corner of our screen.
// Light Parameters
static GLfloat LightAmb[] = {0.7f, 0.7f, 0.7f, 1.0f};// Ambient Light static GLfloat LightDif[] = {1.0f, 1.0f, 1.0f, 1.0f};// Diffuse Light static GLfloat LightPos[] = {4.0f, 4.0f, 6.0f, 1.0f};// Light Position
We set up a variable called q for our quadratic object, xrot and yrot to keep track of rotation. xrotspeed and yrotspeed control the speed our object rotates at. zoom is used to zoom in and out of the scene (we start at -7 which shows us the entire scene) and height is the height of the ball above the floor.
We then make room for our 3 textures with texture[3], and define WndProc().
GLUquadricObj *q;// Quadratic For Drawing A Sphere GLfloat xrot = 0.0f;// X Rotation
GLfloat yrot = 0.0f;// Y Rotation
Lesson 26 – Clipping & Reflections Using The Stencil Buffer 160
GLfloat xrotspeed = 0.0f;// X Rotation Speed GLfloat yrotspeed = 0.0f;// Y Rotation Speed GLfloat zoom = -7.0f;// Depth Into The Screen GLfloat height = 2.0f;// Height Of Ball From Floor GLuint texture[3];// 3 Textures
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);// Declaration For WndProc The ReSizeGLScene() and LoadBMP() code has not changed so I will skip over both sections of code.
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)// Resize And Initialize The GL Window AUX_RGBImageRec *LoadBMP(char *Filename)// Loads A Bitmap Image
The load texture code is pretty standard. You've used it many times before in the previous tutorials. We make room for 3 textures, then we load the three images, and create linear filtered textures from the image data. The bitmap files we use are located in the DATA directory.
int LoadGLTextures()// Load Bitmaps And Convert To Textures {
int Status=FALSE;// Status Indicator
AUX_RGBImageRec *TextureImage[3];// Create Storage Space For The Textures memset(TextureImage,0,sizeof(void *)*3);// Set The Pointer To NULL
if ((TextureImage[0]=LoadBMP("Data/EnvWall.bmp")) &&// Load The Floor Texture (TextureImage[1]=LoadBMP("Data/Ball.bmp")) &&// Load the Light Texture (TextureImage[2]=LoadBMP("Data/EnvRoll.bmp")))// Load the Wall Texture {
Status=TRUE; // Set The Status To TRUE
glGenTextures(3, &texture[0 // Create The Texture for (int loop=0; loop<3; loop++)// Loop Through 5 Textures {
glBindTexture(GL_TEXTURE_2D, texture[loop
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX, TextureImage[loop]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}for (loop=0; loop<3; loop++)// Loop Through 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 }
A new command called glClearStencil is introduced in the init code. Passing 0 as a parameter tells OpenGL to disable clearing of the stencil buffer. You should be familiar with the rest of the code by now. We load our textures and enable smooth shading. The clear color is set to an off blue and the clear depth is set to 1.0f. The stencil clear value is set to 0. We enable depth testing, and set the depth test value to less than or equal to. Our perspective correction is set to nicest (very good quality) and 2d texture mapping is enabled.
int InitGL(GLvoid)// All Setup For OpenGL Goes Here {if (!LoadGLTextures())// If Loading The Textures Failed {
return FALSE;// Return False }
glShadeModel(GL_SMOOTH);// Enable Smooth Shading glClearColor(0.2f, 0.5f, 1.0f, 1.0f);// Background glClearDepth(1.0f);// Depth Buffer Setup
glClearStencil(0);// Clear The Stencil Buffer To 0 glEnable(GL_DEPTH_TEST);// Enables Depth Testing
glDepthFunc(GL_LEQUAL);// The Type Of Depth Testing To Do
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Really Nice Perspective Calculations glEnable(GL_TEXTURE_2D);// Enable 2D Texture Mapping
Now it's time to set up light 0. The first line below tells OpenGL to use the values stored in LightAmb for the Ambient light. If you remember at the beginning of the code, the rgb values of LightAmb were all 0.7f, giving us a white light at 70% full intensity. We then set the Diffuse light using the values stored in LightDif and position the light using the x,y,z values stored in LightPos.
After we have set the light up we can enable it with glEnable(GL_LIGHT0). Even though the light is enabled, you will not see it until we enable lighting with the last line of code.
Note: If we wanted to turn off all lights in a scene we would use glDisable(GL_LIGHTING). If we wanted to disable just one of our lights we would use glDisable(GL_LIGHT{0-7}). This gives us alot of control over the lighting and what lights are on and off. Just
Lesson 26 – Clipping & Reflections Using The Stencil Buffer 161
remember if GL_LIGHTING is disabled, you will not see lights!
glLightfv(GL_LIGHT0, GL_AMBIENT, LightAmb);// Set The Ambient Lighting For Light0 glLightfv(GL_LIGHT0, GL_DIFFUSE, LightDif);// Set The Diffuse Lighting For Light0 glLightfv(GL_LIGHT0, GL_POSITION, LightPos);// Set The Position For Light0 glEnable(GL_LIGHT0);// Enable Light 0
glEnable(GL_LIGHTING);// Enable Lighting
In the first line below, we create a new quadratic object. The second line tells OpenGL to generate smooth normals for our quadratic object, and the third line tells OpenGL to generate texture coordinates for our quadratic. Without the second and third lines of code, our object would use flat shading and we wouldn't be able to texture it.
The fourth and fifth lines tell OpenGL to use the Sphere Mapping algorithm to generate the texture coordinates. This allows us to sphere map the quadratic object.
q = gluNewQuadric();// Create A New Quadratic
gluQuadricNormals(q, GL_SMOOTH);// Generate Smooth Normals For The Quad gluQuadricTexture(q, GL_TRUE);// Enable Texture Coords For The Quad
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);// Set Up Sphere Mapping glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);// Set Up Sphere Mapping return TRUE;// Initialization Went OK
}
The code below will draw our object (which is a cool looking environment mapped beach ball).
We set the color to full intensity white and bind to our BALL texture (the ball texture is a series of red, white and blue stripes).
After selecting our texture, we draw a Quadratic Sphere with a radius of 0.35f, 32 sli ces and 16 stacks (up and down).
void DrawObject()// Draw Our Ball {
glColor3f(1.0f, 1.0f, 1.0f);// Set Color To White
glBindTexture(GL_TEXTURE_2D, texture[1 // Select Texture 2 (1) gluSphere(q, 0.35f, 32, 16);// Draw First Sphere
After drawing the first sphere, we select a new texture (EnvRoll), set the alpha value to 40% and enable blending based on the source alpha value. glEnable(GL_TEXTURE_GEN_S) and glEnable(GL_TEXTURE_GEN_T) enables sphere mapping.
After doing all that, we redraw the sphere, disable sphere mapping and disable blending.
The final result is a reflection that almost looks like bright points of light mapped to the beach ball. Because we enable sphere mapping, the texture is always f acing the viewer, even as the ball spins. We blend so that the new texture doesn't cancel out the old texture (a form of multitexturing).
glBindTexture(GL_TEXTURE_2D, texture[2 // Select Texture 3 (2) glColor4f(1.0f, 1.0f, 1.0f, 0.4f);// Set Color To White With 40% Alpha glEnable(GL_BLEND);// Enable Blending
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Set Blending Mode To Mix Based On SRC Alpha glEnable(GL_TEXTURE_GEN_S);// Enable Sphere Mapping
glEnable(GL_TEXTURE_GEN_T);// Enable Sphere Mapping
gluSphere(q, 0.35f, 32, 16);// Draw Another Sphere Using New Texture // Textures Will Mix Creating A MultiTexture Effect (Reflection) glDisable(GL_TEXTURE_GEN_S);// Disable Sphere Mapping
glDisable(GL_TEXTURE_GEN_T);// Disable Sphere Mapping glDisable(GL_BLEND);// Disable Blending
}
The code below draws the floor that our ball hovers over. We select the floor texture (EnvWall), and draw a single texture mapped quad on the z-axis. Pretty simple!
void DrawFloor()// Draws The Floor {
glBindTexture(GL_TEXTURE_2D, texture[0 // Select Texture 1 (0) glBegin(GL_QUADS);// Begin Drawing A Quad
glNormal3f(0.0, 1.0, 0.0);// Normal Pointing Up glTexCoord2f(0.0f, 1.0f);// Bottom Left Of Texture glVertex3f(-2.0, 0.0, 2.0);// Bottom Left Corner Of Floor glTexCoord2f(0.0f, 0.0f);// Top Left Of Texture
glVertex3f(-2.0, 0.0,-2.0);// Top Left Corner Of Floor glTexCoord2f(1.0f, 0.0f);// Top Right Of Texture glVertex3f( 2.0, 0.0,-2.0);// Top Right Corner Of Floor glTexCoord2f(1.0f, 1.0f);// Bottom Right Of Texture glVertex3f( 2.0, 0.0, 2.0);// Bottom Right Corner Of Floor glEnd();// Done Drawing The Quad
}
Lesson 26 – Clipping & Reflections Using The Stencil Buffer 162
Now for the fun stuff. Here's where we combine all the objects and images to create our reflective scene.
We start off by clearing the screen (GL_COLOR_BUFFER_BIT) to our default clear color (off blue). The depth
(GL_DEPTH_BUFFER_BIT) and stencil (GL_STENCIL_BUFFER_BIT) buffers are also cleared. Make sure you include the stencil buffer code, it's new and easy to overlook! It's important to note when we clear the stencil buffer, we are filling it with 0's.
After clearing the screen and buffers, we define our clipping plane equation. The plane equation is used for clipping the ref lected image.
The equation eqr[]={0.0f,-1.0f, 0.0f, 0.0f} will be used when we draw the reflected image. As you can see, the value for the y-plane is a negative value. Meaning we will only see pixels if they are drawn below the floor or at a negative value on the y-axis. Anything drawn above the floor will not show up when using this equation.
More on clipping later... read on.
int DrawGLScene(GLvoid)// Draw Everything {
// Clear Screen, Depth Buffer & Stencil Buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// Clip Plane Equations
double eqr[] = {0.0f,-1.0f, 0.0f, 0.0f};// Plane Equation To Use For The Reflected Objects
So we have cleared the screen, and defined our clipping planes. Now for the fun stuff!
We start off by resetting the modelview matrix. Which of course starts all drawing in the center of the screen. We then translate down 0.6f units (to add a small perspective tilt to the floor) and into the screen based on the value of zoom. To better explain why we translate down 0.6f units, I'll explain using a simple example. If you were looking at the side of a piece of paper at exactly eye level, you would barely be able to see it. It would more than likely look like a thin line. If you moved the paper down a little, it would no longer look like a l ine. You would see more of t he paper, because your eyes would be looking down at the page instead of directly at the edge of the paper.
glLoadIdentity(); // Reset The Modelview Matrix
glTranslatef(0.0f, -0.6f, zoom);// Zoom And Raise Camera Above The Floor (Up 0.6 Units) Next we set the color mask. Something new to this tutorial! The 4 values for color mask represent red, green, blue and alpha. By default all the values are set to GL_TRUE.
If the red value of glColorMask({red},{green},{blue},{alpha}) was set to GL_TRUE, and all of the other values were 0 (GL_FALSE), the only color that would show up on the screen is red. If the value for red was 0 (GL_FALSE), but the other values were all GL_TRUE, every color except red would be drawn t o the screen.
We don't want anything drawn to the screen at the moment, with all of the values set to 0 (GL_FALSE), colors will not be drawn to the screen.
glColorMask(0,0,0,0);// Set Color Mask
Now even more fun stuff... Setting up the stencil buffer and stencil testing!
We start off by enabling stencil testing. Once stencil testing has been enabled, we are able to modify the stencil buffer.
It's very hard to explain the commands below so please bear with me, and if you have a better explanation, please let me know. In the code below we set up a test. The line glStencilFunc(GL_ALWAYS, 1, 1) tells OpenGL what type of test we want to do on each pixel when an object is drawn t o the screen.
GL_ALWAYS just tells OpenGL the test will always pass. The second parameter (1) is a reference value that we will test in the third line of code, and the third parameter is a mask. The mask is a value that is ANDed with the reference value and stored in the stencil buffer when the test is done. A reference value of 1 ANDed with a mask value of 1 is 1. So if the test goes well and we tell OpenGL to, it will place a one in the stencil buffer (reference&mask=1).
Quick note: Stencil testing is a per pixel test done each ti me an object is drawn to the screen. The reference value ANDed with t he mask value is tested against the current stencil value ANDed with the mask value.
The third line of code tests for three different conditions based on the stencil function we decided to use. The first two parameters are GL_KEEP, and the third is GL_REPLACE.
The first parameter tells OpenGL what to do if the test fails. Because the first parameter is GL_KEEP, if the test fails (which it can't because we have the funtion set to GL_ALWAYS), we would leave the stencil value set at whatever it currently is.
The second parameter tells OpenGL what do do if the stencil test passes, but the depth test fails. In the code below, we eventually disable depth testing so this parameter can be ignored.
The third parameter is the important one. It tells OpenGL what to do if the test passes! In our code we tell OpenGL to replace (GL_REPLACE) the value in the stencil buffer. The value we put into the stencil buffer is our reference value ANDed with our mask value which is 1.
After setting up the type of testing we want to do, we disable depth testing and jump to the code that draws our floor.
Lesson 26 – Clipping & Reflections Using The Stencil Buffer 163
In simple english I will try to sum up everything that the code does up until now...
We tell OpenGL not to draw any colors to the screen. This means that when we draw the floor, it wont show up on the screen.
BUT... each spot on the screen where the object (our floor) should be if we could see it will be tested based on the type of stencil testing we decide to do. The stencil buffer starts out full of 0's (empty). We want to set the stencil value to 1 wherever our object would have been drawn if we could see it. So we tell OpenGL we don't care about testing. If a pixel should have been drawn t o the screen, we want that spot marked with a 1. GL_ALWAYS does exactly that. Our reference and mask values of 1 make sure that the value placed into the stencil buffer is indeed going to be 1 ! As we invisibly draw, our stencil operation checks each pixel location, and replaces the 0 with a 1.
glEnable(GL_STENCIL_TEST);// Enable Stencil Buffer For "marking" The Floor glStencilFunc(GL_ALWAYS, 1, 1);// Always Passes, 1 Bit Plane, 1 As Mask
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);// We Set The Stencil Buffer To 1 Where We Draw Any Polygon
// Keep If Test Fails, Keep If Test Passes But Buffer Test Fails // Replace If Test Passes
glDisable(GL_DEPTH_TEST);// Disable Depth Testing
DrawFloor();// Draw The Floor (Draws To The Stencil Buffer) // We Only Want To Mark It In The Stencil Buffer
So now we have an invisible stencil m ask of the floor. As long as stencil testing is enabled, the only places pixels will show up ar e places where the stencil buffer has a value of 1. All of the pixels on the screen where the invisible floor was drawn will have a stencil value of 1. Meaning as long as stencil testing is enabled, the only pixels that we will see are the pixels that we draw in the same spot our invisible floor was defined in the stencil buffer. The trick behind creating a real looking reflection that reflects in the floor and nowhere else!
So now that we know the ball reflection will only be drawn where the floor should be, it's time to draw the reflection! We enable depth testing, and set the color mask back to all ones (meaning all the colors will be drawn to the screen).
Instead of using GL_ALWAYS for our stencil function we are going to use GL_EQUAL. We'll leave the reference and mask values at 1. For the stencil operation we will set all the parameters to GL_KEEP. In english, any object we draw this time around will actually appear on the screen (because the color mask is set to true for each color). As long as stencil testing is enabled pixels will ONLY be drawn if the stencil buffer has a value of 1 (reference value ANDed with the mask, which is 1 EQUALS (GL_EQUAL) the stencil buffer value ANDed with the mask, which is also 1). If the stencil value is not 1 where the current pixel is being drawn it will not show up! GL_KEEP just tells OpenGL not to modify any values in the stencil buffer if the test passes OR fails!
glEnable(GL_DEPTH_TEST);// Enable Depth Testing
glColorMask(1,1,1,1);// Set Color Mask to TRUE, TRUE, TRUE, TRUE glStencilFunc(GL_EQUAL, 1, 1);// We Draw Only Where The Stencil Is 1 // (I.E. Where The Floor Was Drawn)
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);// Don't Change The Stencil Buffer
Now we enable the mirrored clipping plane. This plane is defined by eqr, and only allows object to be drawn from the center of the screen (where the floor is) down to the bottom of the screen (any negative value on the y-axis). That way the reflected ball that we draw can't come up through the center of the floor. That would look pretty bad if it did. If you don't understand what I mean, remove the first line below from the source code, and move the real ball (non reflected) through the floor. If clipping is not enabled, you will see the reflected ball pop out of the floor as the real ball goes into the floor.
After we enable clipping plane0 (usually you can have from 0-5 clipping planes), we define the plane by telling it to use the parameters stored in eqr.
We push the matrix (which basically saves the position of everything on the screen) and use glScalef(1.0f,-1.0f,1.0f) to flip the object upside down (creating a real looking reflection). Setting the y value of glScalef({x},{y},{z}) to a negative value forces OpenGL to render opposite on the y-axis. It's almost like flipping the entire screen upside down. When position an object at a positive value on the y-axis, it will appear at the bottom of the screen instead of at the top. When you rotate an object towards
We push the matrix (which basically saves the position of everything on the screen) and use glScalef(1.0f,-1.0f,1.0f) to flip the object upside down (creating a real looking reflection). Setting the y value of glScalef({x},{y},{z}) to a negative value forces OpenGL to render opposite on the y-axis. It's almost like flipping the entire screen upside down. When position an object at a positive value on the y-axis, it will appear at the bottom of the screen instead of at the top. When you rotate an object towards