OpenGL:Tutorials:Tutorial Framework:Light and Fog

From GPWiki
Jump to: navigation, search

This tutorial deals with lighting and fog effects. Lighting adds realistic visual cues to your scene, but it can be costly on the frame rate. Effective use of fog can reduce the polycount by shortening the visible distance of our view without 'pop-up' destroying the realism of the scene.

Warning, Deprecated!

The OpenGL programming technics presented on this webpage are deprecated in the last versions of OpenGL.

Setting Fog Parameters

OpenGL supports 'fogging' objects, which means it applies a fog color to stuff that is further away. If your geometry does not fill the whole screen it is advisable to use the same color for fog and for the background, so stuff actually fades away instead of becoming a weird silhouette in the distance. Fog is switched on like many other OpenGL options:

  glEnable(GL_FOG);

Tweaking the parameters of the fog effect is performed using the glFog() function. Again, a multi-flavour function, check your documentation for all the possible options. Here we use glFogfv() to set the fog color using an array of float values:

  float FogCol[3]={0.8f,0.8f,0.8f}; // Define a nice light grey
  glFogfv(GL_FOG_COLOR,FogCol);     // Set the fog color

There are two ways in which OpenGL can apply fog - linear and exponential. For linear fog, you need to do something like the following:

  glFogi(GL_FOG_MODE, GL_LINEAR); // Note the 'i' after glFog - the GL_LINEAR constant is an integer.
  glFogf(GL_FOG_START, 10.f);
  glFogf(GL_FOG_END, 40.f);

For the amount of fogginess that a point gets the distance from the camera to that point is used. These parameters create a fog starting at 10 units from the camera (so close objects retain their original color). At 40 units from the camera the fog completely colors objects, making them disappear if the background happens to be the same color as the fog. The default values for these distances are 0 and 1, which is much too close for most scenes.

The exact formula for the amound of fog a point gets in linear fog mode is: F = (end - z) / (end - begin), where F is the amount of fog and z is the distance between the point and the camera.

For exponential fog use:

  glFogi(GL_FOG_MODE, GL_EXP);
  // or
  glFogi(GL_FOG_MODE, GL_EXP2);

This creates a fog that starts right at the camera but progresses in an exponential way. The formulae for these modes are F = e^(-D * z) for GL_EXP and F = e^((-D * z)^2) for GL_EXP2. (to be done: make pretty latex math out of that) The D in there is a constant - the fog density - that defaults to 1 and can be changed like this:

  glFogf(GL_FOG_DENSITY, 2.f);

As mentioned above, the use of fog allows us to reduce the distance to the far clipping plane because faraway stuff will be invisible anyway. The following change to the screen setup brings the far plane a little closer and reduces the work our GPU has to do.

  gluPerspective(45.f, 800.f / 600.f, 1.f, 60.0f); // Fog allows us to shorten the far clipping plane

Setting Up Lighting

So, can you guess how we switch lighting on?

  glEnable(GL_LIGHTING);

We also need to enable the light source:

  glEnable(GL_LIGHT0);

The OpenGL standard specifies that 8 lights ( GL_LIGHT0 - GL_LIGHT7 ) should be supported. However, the actual number varies depending on your implemetation. Using anything more than 2 lights in your scene will kill the frame rate anyway.

We set the parameters for the lights in much the same way that we did with the fog:

  float LightPos[4]={-5.0f,5.0f,10.0f,0.0f};
  float Ambient[4]={0.5f,0.5f,0.5f,1.0f};
 
  glLightfv(GL_LIGHT0,GL_POSITION,LightPos);
  glLightfv(GL_LIGHT0,GL_AMBIENT,Ambient);

Here we use glLight() to position the light above, behind and to the left of the observer. We also set the ambient light level to prevent unlit areas from being too dark. The default settings for the light's diffuse and specular values are (1.0,1.0,1.0,1.0), this is fine for our application.


A Word About Normals

Now we have some light, we can shade our object's faces. However, this is not automatic, the normal vector of a polygon determines the 'facing' of the poly in relation to the light, which in turn affects the shade of the face. We must calculate the normal vectors and pass them to OpenGL using glNormal().

I've placed a (hastily written) function in render.h to calculate normals.

// Calculate normal from vertices
stVec CalcNormal(stVec v1, stVec v2, stVec v3)
 {
  double v1x,v1y,v1z,v2x,v2y,v2z;
  double nx,ny,nz;
  double vLen;
 
  stVec Result;
 
  // Calculate vectors
  v1x = v1.x - v2.x;
  v1y = v1.y - v2.y;
  v1z = v1.z - v2.z;
 
  v2x = v2.x - v3.x;
  v2y = v2.y - v3.y;
  v2z = v2.z - v3.z;
 
  // Get cross product of vectors
  nx = (v1y * v2z) - (v1z * v2y);
  ny = (v1z * v2x) - (v1x * v2z);
  nz = (v1x * v2y) - (v1y * v2x);
 
  // Normalise final vector
  vLen = sqrt( (nx * nx) + (ny * ny) + (nz * nz) );
 
  Result.x = (float)(nx / vLen);
  Result.y = (float)(ny / vLen);
  Result.z = (float)(nz / vLen);
 
  return Result;
 }
</code>
I'm not going to explain normal vectors here, Marijn's already covered them [[Vector|here]]. Just accept that if you pass in three points of your polygon in counter-clockwise order, this function will return the normal vector.
 
 
In order to associate the normal with the polygons, we use the same method as colors and texture coords. We specify a normal vector and it will be used by OpenGL until we specify another.
<code type="cpp">
   // Calc the face normal
   p1.x=-10.0f; p1.y=-2.0f; p1.z= 10.0f;
   p2.x= 10.0f; p2.y=-2.0f; p2.z= 10.0f;
   p3.x= 10.0f; p3.y=-2.0f; p3.z=-10.0f;
   Norm=CalcNormal(p1,p2,p3);
 
   glNormal3f(Norm.x,Norm.y,Norm.z);  // Here's the normal call
 
   // Draw floor plane
   glBegin(GL_QUADS);
    glTexCoord2f( 0.0f,20.0f); glVertex3f(-50.0f,-3.0f, 50.0f);
    glTexCoord2f( 0.0f, 0.0f); glVertex3f( 50.0f,-3.0f, 50.0f);
    glTexCoord2f(20.0f, 0.0f); glVertex3f( 50.0f,-3.0f,-50.0f);
    glTexCoord2f(20.0f,20.0f); glVertex3f(-50.0f,-3.0f,-50.0f);
   glEnd();

BTW. I've gone back to quads on this demo as setting normals on the triangle strip was tricky.

We only need to specify one normal per face to get flat shading. If we had used a curved surface like a sphere, the normals are specified on a per-vertex basis to achieve the smooth shade effect.

== Subdividing == (not needed anymore)

For lighting and fog to look good it was often a good idea to make sure your geometry does not contain any huge polygons. Light and fog are applied per vertex, and the situation at the corners/vertices may not be a very good representation of the situation in the middle of the polygon. For example, if you have a big wall with a light in the middle, the wall will appear too dark. The corners are far away from the light, and the lightness of the whole wall is interpolated between those corners. The solution to this is to divide big polygons like these into a bunch of smaller polygons. Often simply dividing them into a grid or similar works well. To decide the number of subdivisions you have to weight the improved quality of the render against the decrease in performance caused by the extra polygons.

Because of the fact, that performance suffers due to increasing polygoncounts, it's better to use a more modern technology. Gouraud Shading offers a fast way to handle the problem described above, by interpolating colors across triangles. [ glShadeModel(GL_SMOOTH) ]

A step into the future is Phong Shading. It interpolates a normalvector from the three given normals across the surface of a triangle for each pixel. You will need a pixelshader for it, like it is included in Rendermonkey and FX-Composer, if you want to create the shader yourself: http://wiki.truevision3d.com/hlsl_phong_sample

Source Code

Here's a screenshot of the demo
GLTut4LitCube.jpg
Press 'f' to toggle the fog, use the up and down keys to adjust the cube distance.

The source to Render.cpp, compile this demo using the OpenGL Tutorial Framework.

#include "Framework.h"
#include <math.h>
#include "tga.h"
 
// Vertex / Vector struct
typedef struct
 {
  float x,y,z;
 }stVec;
 
// Function declarations
GLuint LoadTexture(char *TexName);
stVec CalcNormal(stVec v1, stVec v2, stVec v3);
 
// Here we go!
void Render(void)
 {
  GLuint TexID,TexID2; // Handles to our textures
  float Rotate=0.0f;   // Rotation value to be used to spin our polygon
  float zPos=-5.0f;    // The distance to our cube
  stVec p1,p2,p3,Norm; // Vertices and vector variables
 
  // Fog parameters
  bool FogFlag=true;
  float FogCol[3]={0.8f,0.8f,0.8f};  // define a nice light grey
 
  // Light parameters
  float LightPos[4]={-5.0f,5.0f,10.0f,0.0f};
  float Ambient[4]={0.5f,0.5f,0.5f,1.0f};
 
  // Background color
  glClearColor(0.8f,0.8f,0.8f,1.0f);
 
  // Setup our screen
  glViewport(0,0,800,600);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(45.0f,800.0f/600.0f,1.0f,60.0f); // Fog allows us to shorten the far clipping plane
  glMatrixMode(GL_MODELVIEW);
 
  // Ensure correct display of polygons
  glEnable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);
  glDepthMask(GL_TRUE);
 
  // Turn On Fog
  glEnable(GL_FOG);
  glFogfv(GL_FOG_COLOR,FogCol); // Set the fog color
  glFogf(GL_FOG_DENSITY,0.1f);  // Thin the fog out a little
 
  // Turn on Lighting
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glLightfv(GL_LIGHT0,GL_POSITION,LightPos);
  glLightfv(GL_LIGHT0,GL_AMBIENT,Ambient);
 
  // Load the texture
  TexID=LoadTexture("Logo.tga");
  TexID2=LoadTexture("Check.tga");
 
  // This loop will run until Esc is pressed
   while(RunLevel)
    {
      if(Keys[VK_ESCAPE]) // Esc Key
       RunLevel=0;
 
      if(Keys[VK_UP]) // Up Arrow
       {
        zPos-=0.01f;
         if(zPos < -50.0f) // Limit movement
          zPos= -50.0f;
       }
 
      if(Keys[VK_DOWN]) // Down Arrow
       {
        zPos+=0.01f;
         if(zPos > -3.0f) // Limit movement
          zPos= -3.0f;
       }
 
      if(Keys['F']) // Toggle Fog
       {
        FogFlag ^= true;
        Keys[70]=false;
       }
 
 
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glLoadIdentity(); // Reset current matrix (Modelview)
 
      // Fog On or Off
      if(FogFlag)
       glEnable(GL_FOG);
      else
       glDisable(GL_FOG);
 
     // Enable texturing and select floor texture
     glColor3f(1.0f,1.0f,1.0f);
     glEnable(GL_TEXTURE_2D);
     glBindTexture(GL_TEXTURE_2D,TexID2);
 
     // Calc the face normal
     p1.x=-10.0f; p1.y=-2.0f; p1.z= 10.0f;
     p2.x= 10.0f; p2.y=-2.0f; p2.z= 10.0f;
     p3.x= 10.0f; p3.y=-2.0f; p3.z=-10.0f;
     Norm=CalcNormal(p1,p2,p3);
     glNormal3f(Norm.x,Norm.y,Norm.z);
 
     // Draw floor plane, note the use of GL_REPEAT and tex coords outside the 0.0 - 1.0 range
     glBegin(GL_QUADS);
      glTexCoord2f( 0.0f,20.0f); glVertex3f(-50.0f,-3.0f, 50.0f);
      glTexCoord2f( 0.0f, 0.0f); glVertex3f( 50.0f,-3.0f, 50.0f);
      glTexCoord2f(20.0f, 0.0f); glVertex3f( 50.0f,-3.0f,-50.0f);
      glTexCoord2f(20.0f,20.0f); glVertex3f(-50.0f,-3.0f,-50.0f);
     glEnd();
 
     // Do our rotations
     glTranslatef(0.0f,0.0f,zPos);
     glRotatef(Rotate,0.0f,0.0f,1.0f);
     glRotatef(Rotate,1.0f,0.6f,0.0f);
 
     // Select cube texture
     glBindTexture(GL_TEXTURE_2D,TexID);
 
     // Calc the top face normal
     p1.x=-1.0f; p1.y= 1.0f; p1.z= 1.0f;
     p2.x= 1.0f; p2.y= 1.0f; p2.z= 1.0f;
     p3.x= 1.0f; p3.y= 1.0f; p3.z=-1.0f;
     Norm=CalcNormal(p1,p2,p3);
 
     glNormal3f(Norm.x,Norm.y,Norm.z);
 
     // Draw the top face
     glBegin(GL_QUADS);
      glTexCoord2f(1.0f,1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
      glTexCoord2f(1.0f,0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
      glTexCoord2f(0.0f,0.0f); glVertex3f( 1.0f, 1.0f,-1.0f);
      glTexCoord2f(0.0f,1.0f); glVertex3f(-1.0f, 1.0f,-1.0f);
 
      // Calc the bottom face normal
      p1.x=-1.0f; p1.y=-1.0f; p1.z= 1.0f;
      p2.x=-1.0f; p2.y=-1.0f; p2.z=-1.0f;
      p3.x= 1.0f; p3.y=-1.0f; p3.z=-1.0f;
      Norm=CalcNormal(p1,p2,p3);
 
      glNormal3f(Norm.x,Norm.y,Norm.z);
 
      // Draw bottom face
      glTexCoord2f(1.0f,0.0f); glVertex3f(-1.0f,-1.0f, 1.0f);
      glTexCoord2f(0.0f,0.0f); glVertex3f(-1.0f,-1.0f,-1.0f);
      glTexCoord2f(0.0f,1.0f); glVertex3f( 1.0f,-1.0f,-1.0f);
      glTexCoord2f(1.0f,1.0f); glVertex3f( 1.0f,-1.0f, 1.0f);
 
      // Calc the first side face normal
      p1.x=-1.0f; p1.y= 1.0f; p1.z= 1.0f;
      p2.x=-1.0f; p2.y=-1.0f; p2.z= 1.0f;
      p3.x= 1.0f; p3.y= 1.0f; p3.z= 1.0f;
      Norm=CalcNormal(p1,p2,p3);
 
      glNormal3f(Norm.x,Norm.y,Norm.z);
 
      glTexCoord2f(1.0f,0.0f);  glVertex3f(-1.0f, 1.0f, 1.0f);
      glTexCoord2f(0.0f,0.0f);  glVertex3f(-1.0f,-1.0f, 1.0f);
      glTexCoord2f(0.0f,1.0f);  glVertex3f( 1.0f,-1.0f, 1.0f);
      glTexCoord2f(1.0f,1.0f);  glVertex3f( 1.0f, 1.0f, 1.0f);
 
      // Next face
      p1.x= 1.0f; p1.y= 1.0f; p1.z= 1.0f;
      p2.x= 1.0f; p2.y=-1.0f; p2.z= 1.0f;
      p3.x= 1.0f; p3.y= 1.0f; p3.z=-1.0f;
      Norm=CalcNormal(p1,p2,p3);
 
      glNormal3f(Norm.x,Norm.y,Norm.z);
 
      glTexCoord2f(1.0f,1.0f);  glVertex3f( 1.0f, 1.0f, 1.0f);
      glTexCoord2f(0.0f,1.0f);  glVertex3f( 1.0f,-1.0f, 1.0f);
      glTexCoord2f(0.0f,2.0f);  glVertex3f( 1.0f,-1.0f,-1.0f);
      glTexCoord2f(1.0f,2.0f);  glVertex3f( 1.0f, 1.0f,-1.0f);
 
      // Next face
      p1.x= 1.0f; p1.y=-1.0f; p1.z=-1.0f;
      p2.x=-1.0f; p2.y=-1.0f; p2.z=-1.0f;
      p3.x=-1.0f; p3.y= 1.0f; p3.z=-1.0f;
      Norm=CalcNormal(p1,p2,p3);
 
      glNormal3f(Norm.x,Norm.y,Norm.z);
 
      glTexCoord2f(0.0f,2.0f);  glVertex3f( 1.0f,-1.0f,-1.0f);
      glTexCoord2f(0.0f,3.0f);  glVertex3f(-1.0f,-1.0f,-1.0f);
      glTexCoord2f(1.0f,3.0f);  glVertex3f(-1.0f, 1.0f,-1.0f);
      glTexCoord2f(1.0f,2.0f);  glVertex3f( 1.0f, 1.0f,-1.0f);
 
     // Next face
      p1.x=-1.0f; p1.y=-1.0f; p1.z=-1.0f;
      p2.x=-1.0f; p2.y=-1.0f; p2.z= 1.0f;
      p3.x=-1.0f; p3.y= 1.0f; p3.z= 1.0f;
      Norm=CalcNormal(p1,p2,p3);
 
      glNormal3f(Norm.x,Norm.y,Norm.z);
 
      glTexCoord2f(0.0f,3.0f);  glVertex3f(-1.0f,-1.0f,-1.0f);
      glTexCoord2f(1.0f,3.0f);  glVertex3f(-1.0f,-1.0f, 1.0f);
      glTexCoord2f(1.0f,4.0f);  glVertex3f(-1.0f, 1.0f, 1.0f);
      glTexCoord2f(0.0f,4.0f);  glVertex3f(-1.0f, 1.0f,-1.0f);
     glEnd();
 
     // Add to the rotation for next frame 
     Rotate+=0.05f;
 
     // Show our cube
     FlipBuffers();
    }
 
  // Clean up textures
  glDeleteTextures(1,&TexID);
  glDeleteTextures(1,&TexID2);
 }
 
 
// Calculate normal from vertices
stVec CalcNormal(stVec v1, stVec v2, stVec v3)
 {
  double v1x,v1y,v1z,v2x,v2y,v2z;
  double nx,ny,nz;
  double vLen;
 
  stVec Result;
 
  // Calculate vectors
  v1x = v1.x - v2.x;
  v1y = v1.y - v2.y;
  v1z = v1.z - v2.z;
 
  v2x = v2.x - v3.x;
  v2y = v2.y - v3.y;
  v2z = v2.z - v3.z;
 
  // Get cross product of vectors
  nx = (v1y * v2z) - (v1z * v2y);
  ny = (v1z * v2x) - (v1x * v2z);
  nz = (v1x * v2y) - (v1y * v2x);
 
  // Normalise final vector
  vLen = sqrt( (nx * nx) + (ny * ny) + (nz * nz) );
 
  Result.x = (float)(nx / vLen);
  Result.y = (float)(ny / vLen);
  Result.z = (float)(nz / vLen);
 
  return Result;
 }
 
 
// Load a TGA texture
GLuint LoadTexture(char *TexName)
 {
  TGAImg Img;        // Image loader
  GLuint Texture;
 
  // Load our Texture
   if(Img.Load(TexName)!=IMG_OK)
    return -1;
 
  glGenTextures(1,&Texture);            // Allocate space for texture
  glBindTexture(GL_TEXTURE_2D,Texture); // Set our Tex handle as current
 
  // Create the texture
   if(Img.GetBPP()==24)
    glTexImage2D(GL_TEXTURE_2D,0,3,Img.GetWidth(),Img.GetHeight(),0,
                 GL_RGB,GL_UNSIGNED_BYTE,Img.GetImg());
   else if(Img.GetBPP()==32)
    glTexImage2D(GL_TEXTURE_2D,0,4,Img.GetWidth(),Img.GetHeight(),0,
                 GL_RGBA,GL_UNSIGNED_BYTE,Img.GetImg());
   else
    return -1;
 
  // Specify filtering and edge actions
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
 
  return Texture;
 }

Downloads

OpenGL_Tut4_(Lighting).zip - A zip including all source code, image files and Win32 exe.