If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource |
Extending on the previous tutorial's spinning cube code, this tutorial will explain how to calculate your cube's face normals and add OpenGL lighting to your program.
When you've completed this tutorial, you should have a good enough understanding of the of OpenGL lighting to be able to use it in your own code.
We'll be using the same code as we used in the previous tutorial, so load it up and we'll begin.
The Code
The first thing we'll do is add normals to our cube quad struct. Normals are vectors of length 1 which indicate the angle of facing of a polygon. The cube struct should now look like this:
struct { struct { float pos[3]; float col[3]; }ver[8]; struct { unsigned int ver[6]; float norm[3]; }quad[6]; }cube;As we're going to be doing lighting, we'll need a way to store on our lights. Because we might want to have more than one light, it makes sense to use a typedef struct.
typedef struct { float pos[4]; float diffuse[4]; float specular[4]; float ambient[4]; }light_t;We'll then create an instance of light_t and fill it like this, to create a plain white positional light, positioned at (6,10,15).
light_t light={ {6,10,15,1}, //position (the final 1 means the light is positional) {1,1,1,1}, //diffuse {0,0,0,1}, //specular {0,0,0,1} //ambient };Next we need to add a few lines to our initialization in order to set up the lighting in our program. glEnable(GL_LIGHTING) is fairly self explanatory, I hope. glEnable(GL_LIGHT0) enables light 0 (there are 8 to choose from). glLightModeli( GL_LIGHT_MODEL_TWO_SIDE, 0) specifies that we only want OpenGL to calculate lighting on front faces, ignoring the invisible back ones.
glEnable(GL_LIGHTING); //enables lighting glEnable(GL_LIGHT0); //enables a light glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,0); //sets lighting to one-sidedWhen GL_LIGHTING is enabled, vertices' colours are specified by their material (which holds specular, diffuse and ambient lighting properties) rather than the current colour, however, if we call glEnable(GL_COLOR_MATERIAL) it will cause the current material to "track" the current colour, meaning our calls to glColor3f() will still work.
glEnable(GL_COLOR_MATERIAL);If we left it at that, the ambient and diffuse properties of the material would track the current colour. We don't want that, however, because the ambient colour would cause our cube to glow like something out of Chernobyl, so we'll call glColorMaterial(GL_FRONT,GL_DIFFUSE), which will cause only the diffuse property to track the current colour, resulting in a much healthier looking cube.
glColorMaterial(GL_FRONT,GL_DIFFUSE);Finally, we'll need to make sure that the current material's ambient and specular properties are set to black, as we don't want (or at least I don't want) the cube to be self-illuminating or reflective.
float black[4]={0,0,0,0}; glMaterialfv(GL_FRONT,GL_AMBIENT,black); glMaterialfv(GL_FRONT,GL_SPECULAR,black);Next we're going to need to calculate the normals. While we could calculate them by hand very easily (top is (0,1,0) etc) in the case of our cube, it would be useful to be able to calculate them for any given polygon, so we'll need to create some new functions. Add the following declarations in the global scope:
void crossProduct(float *c,float a[3], float b[3]); void normalize(float *vect); void getFaceNormal(float *norm,float pointa[3],float pointb[3],float pointc[3]);The function getFaceNormal() will calculate the normal for any face given three points in clockwise order. When it's done it'll store the normal in a place pointed to by norm.
The first thing it'll do is copy the points pointa, pointb and pointc into a 2-dimensional array, point[ ] [ ].
void getFaceNormal(float *norm,float pointa[3],float pointb[3],float pointc[3]) { float vect[2][3]; int a,b; float point[3][3]; for (a=0;a<3;++a) { point[0][a]=pointa[a]; //copies points into point[][] point[1][a]=pointb[a]; point[2][a]=pointc[a]; }Next, we need to calculate two vectors; one going from point[0] to point[1] and the other going from point[0] to point[2]. We can find them by subtracting point[0] from point[1] and then point[0] from point[2]. I'm not sure why I decided code it like this; I'm sure there's an easier way:
for (a=0;a<2;++a) //calculates vectors from point[0] to point[1] { //and point[0] to point[2] for (b=0;b<3;++b) { vect[a][b]=point[2-a][b]-point[0][b]; } }Finally, we need to find the cross product of the 2 vectors to find the vector at 90º to them, and then normalise the result (make it of length 1). These functions aren't written yet, but I'll get there.
crossProduct(norm,vect[0],vect[1]); //calculates vector at 90° to to 2 vectors normalize(norm); //makes the vector length 1 }The crossProduct() function calculates the cross product of the 2 vectors and sends the result to c.
void crossProduct(float *c,float a[3], float b[3]) //finds the cross product of two vectors { c[0]=a[1]*b[2] - b[1]*a[2]; c[1]=a[2]*b[0] - b[2]*a[0]; c[2]=a[0]*b[1] - b[0]*a[1]; }The normalise() function finds the length of the vector pointed to by the pointer vect using Pythagoras' Theorem extended slightly: A² + B² + C² = D², where A, B and C are the vector's dimensions in X, Y and Z, and D is the length of the vector. Use of sqrt() requires we include math.h in our code, so we'll need to add it to the header includes.
length=sqrt( //A^2 + B^2 + C^2 = length^2 pow(vect[0],2)+ pow(vect[1],2)+ pow(vect[2],2) );The function then divides the vector by length to normalise it. Whilst we could at this point check if length equals 0 to prevent divides by zero, its probably not necessary as we shouldn't be sending this function vectors with no length anyway.
for (a=0;a<3;++a) { vect[a]/=length; }Next we'll add normal calculation to our initCube() function. At the bottom of the function, just before the closing brace, we'll add the following code, which calculates the normals for every quad and stores the result in cube.quad[ ].norm.
for (a=0;a<6;++a) { getFaceNormal(cube.quad[a].norm, cube.ver[ cube.quad[a].ver[2] ].pos, cube.ver[ cube.quad[a].ver[1] ].pos, cube.ver[ cube.quad[a].ver[0] ].pos); }Now, in our redraw function, we need to position the lights. Although we could just do this in our initialise function, we may want to update the position or colour of the light at some time in the main loop of the program, so we need it here. Because we're specifying the position of the light before we translate the modelview matrix for drawing the cube, the position will change relative to the cube as the cube rotates. We'll add the code just after the call to glClear().
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLightfv(GL_LIGHT0,GL_POSITION,light.pos); //updates the light's position glLightfv(GL_LIGHT0,GL_DIFFUSE,light.diffuse); //updates the light's diffuse colour glLightfv(GL_LIGHT0,GL_SPECULAR,light.specular); //updates the light's specular colour glLightfv(GL_LIGHT0,GL_AMBIENT,light.ambient); //updates the light's ambient colour glPushMatrix();All we need now is to draw our cube with its normals. In our redraw function at the beginning of the quad loop (just after the line that says "for (a=0;a<6;++a)"), we'll add the glNormal command which will specify the normals for each quad.
for (a=0;a<6;++a) { glNormal3fv(cube.quad[ a ].norm); //sets the current normal to this quad's normal for (b=0;b<4;++b) { currentVer=cube.quad[a].ver[b];And we're done. Run the code and see for yourself. OpenGL can simulate a lot of different effects and materials with its lighting engine, so its worth experimenting with different lighting settings and material properties to see the different effects you can get. If you want to offer some feedback on this article, or if you have any questions or requests for future tutorials, please email me.
This tutorial is Copyright © 2001 Ben Woodhouse