3D Animation with OpenGL
Port hole view
Link to launch bay
Launch bay
Above: a view through the port hole.

Bot Reject has been working on some 3D animated
graphics with OpenGL and VC++. This is a simple
demonstration of 3D texturing and animation using
OpenGL. The program is not yet optimised for speed
(though it runs fine on my computer, apologies in
advance if it is slow on yours) and there is no collision
detection incorporated yet (so you can walk through
walls!) but this is being developed. Download the C++
executable (written for Windows XP, not sure if it will
run on other systems) by clicking
3DLaunch_Bay_v2.

Note, that since this is an executable your browser
security may block a download until you click the
appropriate option - this is a legitimate program and
not malware, so don't worry!

Use the left/right cursor keys and Page Up and Page
Down keys to explore the launch bay! The up/down
cursor keys enable you to look up/down.

The textures for this map were rendered in Pov-Ray.

What we need now is something to launch from the
launch bay!

Bot will soon be explaining to you how the code works
- not that he is an expert in OpenGL (at least not yet)
but it is hard to find simple code that does simple
things!
Back to games programming intro ...
Under construction...

Coming up:

  • Building a room in OpenGL - geometry and adding your own bitmaps as textures.
  • Moving the viewer around the room.
  • Lighting.
  • Collision detection (when I get it working).
  • ...

Building a room in OpenGL

For my projects I usually use VC++ (Visual C++, i.e. C++ as written in MS Visual Studio, of which I have the
2003 version) with or without MFC (Microsoft Foundation Class library) support - in this case with. This
allows us to use Microsoft's useful bitmap handling functions and sound-handling functions, etc.

The header files and global variables are shown below:
Port hole view
closed door
Hanger bay view
#include "stdafx.h"
#include "3DGallery.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// The one and only application object

CWinApp theApp;

#include <cstdio>
#include <GL/glut.h>
using namespace std;
#include <cmath>

//#include <stdlib.h>
//#include <GL/glut.h>

//Global variables

GLfloat vertices[][3] = {

{-20.0,-10.0,-20.0}, {20.0,-10.0,-20.0}, {20.0,10.0,-20.0}, {-20.0,10.0,-20.0},
{-20.0,-10.0,20.0}, {20.0,-10.0,20.0}, {20.0,10.0,20.0}, {-20.0,10.0,20.0},
{60.0,-10.0,80.0}, {60.0,10.0,80.0}, {-60.0,-10.0,80.0}, {-60.0,10.0,80.0},
{60.0,-10.0,40.0}, {60.0,10.0,40.0}, {-60.0,-10.0,40.0}, {-60.0,10.0,40.0},

{10.0,-10.0,40.0}, {10.0,10.0,40.0}, {-10.0,-10.0,40.0}, {-10.0,10.0,40.0},
{10.0,-10.0,20.0}, {10.0,10.0,20.0}, {-10.0,-10.0,20.0}, {-10.0,10.0,20.0},
{10.0,-10.0,40.0}, {10.0,10.0,40.0}, {-10.0,-10.0,40.0}, {-10.0,10.0,40.0},
{10.0,-10.0,20.0}, {10.0,10.0,20.0}, {-10.0,-10.0,20.0}, {-10.0,10.0,20.0},

};

GLfloat normals[][3] = {{-1.0,-1.0,-1.0},{1.0,-1.0,-1.0},
{1.0,1.0,-1.0}, {-1.0,1.0,-1.0}, {-1.0,-1.0,1.0},
{1.0,-1.0,1.0}, {1.0,1.0,1.0}, {-1.0,1.0,1.0}};

GLfloat colors[][4] = {{0.0,0.0,0.0,1.0},{1.0,0.0,0.0,1.0},
{1.0,1.0,0.0,1.0}, {0.0,1.0,0.0,1.0}, {0.0,0.0,1.0,1.0},
{1.0,0.0,1.0,1.0}, {1.0,1.0,1.0,1.0}, {0.0,1.0,1.0,1.0}};

GLfloat eyeX = 0.0, eyeY = 0.0, eyeZ = 0.0; //Viewing coord origin
GLfloat atX = 0.0, atY = 0.0, atZ = -1.0; //Look-at point
GLfloat upX = 0.0, upY = 1.0, upZ = 0.0; //View-up vector

//Near and far clipping planes
GLfloat dnear = 2-1.0, dfar = 100.0;

GLint bitMapSize = 512;
GLubyte my_texels[512*4][512*4];

static GLdouble viewer[]= {eyeX, eyeY, eyeZ}; // initial viewer location
static GLdouble look_at[] = {atX, atY, atZ}; // initial look at location

const float pi = acos(-1.f);
Since I opted for MFC support, some of the header code was put in by Visual Studio (VS), including:

#include "stdafx.h"

which includes standard MS windows/MFC functions. CWinApp theApp; is also added by VS, along with
3DGallery.h (a header with the same name as the application itself) and the debug definitions.

In this kind of code we may have variables defined in C++ syntax or in OpenGL syntax. The prefix 'GL'
indicates an OpenGL variable, so the global variable GLfloat vertices[][3] is an OpenGL 2D array of float
values which contains an unspecified number of 3-tuples or triplets, which are the (x,y,z) position vectors
of the corners of the two rooms and joining corridor that make-up this scene.

We also have 2D arrays holding normals and colours; note the GLfloats specifying the initial (x,y,z)
coordinates of the 'eye' or viewer, since we are using a first-person view this is essentially the position of
the camera. The at values tell the camera in which direction to look; whatever direction this is, the distance
between the eye vector and the look at vector is kept fixed to 1 unit at all times. The view-up vector tells
OpenGL the rotation of the camera, in this case the camera is level, so its up-vector points straight-up (in
the +y direction).

Note also the calculation of the constant pi (3.14...) as arccos(-1) - check it on a calculator! The code
below follows straight on from the above:
void polygon(int a, int b, int c , int d , int e, short tex)
{
CBitmap Image;
int imgSize = 512;
bool Tile = false;

try {
switch (tex)
{
  case 1:                        
          Image.LoadBitmap(IDB_BITMAP1);
          Image.GetBitmapBits((512*512)*4,my_texels);
          imgSize = 512;
          break;
  case 2:
          Image.LoadBitmap(IDB_BITMAP2);                        
          Image.GetBitmapBits((64*64)*4,my_texels);                                
          imgSize = 64;
          Tile = true;
          break;
  case 3:
          Image.LoadBitmap(IDB_BITMAP6);
          Image.GetBitmapBits((512*512)*4,my_texels);
          imgSize = 512;
          break;
  case 4:
          Image.LoadBitmap(IDB_BITMAP8);
          Image.GetBitmapBits((512*512)*4,my_texels);
          imgSize = 512;
          break;
  case 5:
          Image.LoadBitmap(IDB_BITMAP9);
          Image.GetBitmapBits((512*512)*4,my_texels);
          imgSize = 512;
          break;
  default:
          Image.LoadBitmap(IDB_BITMAP5);
          Image.GetBitmapBits((512*512)*4,my_texels);
          imgSize = 512;
          break;
}
}
catch(...)
{
  //Error accessing resources!
}

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); //Default?
glTexImage2D(GL_TEXTURE_2D, 0, 4, imgSize, imgSize, 0, GL_RGBA,   
GL_UNSIGNED_BYTE, my_texels);
glEnable(GL_TEXTURE_2D);

if(Tile == false)
{
  GLfloat specularCoeff[] = { 1.0, 1.0, 1.0, 1.0 };
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, colors[e]);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specularCoeff);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 25.0);
  
  glBegin(GL_POLYGON);                
          //glColor3fv(colors[a]);                        
          glNormal3fv(normals[a]);
          glTexCoord2f(0.0, 0.0);        
          glVertex3fv(vertices[a]);
          //glColor3fv(colors[b]);                        
          glNormal3fv(normals[b]);
          glTexCoord2f(1.0, 0.0);        
          glVertex3fv(vertices[b]);
          //glColor3fv(colors[c]);                        
          glNormal3fv(normals[c]);
          glTexCoord2f(1.0, 1.0);
          glVertex3fv(vertices[c]);
          //glColor3fv(colors[d]);                        
          glNormal3fv(normals[d]);
          glTexCoord2f(0.0, 1.0);                
          glVertex3fv(vertices[d]);

  glEnd();
}
else //Tile == true
{
  int numTilesX = 8;
  int numTilesZ = 8;
  //CBitmap largeImage;
  GLfloat specularCoeff[] = { 1.0, 1.0, 1.0, 1.0 };
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, colors[e]);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specularCoeff);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 25.0);
  for(int Z = 1; Z <= numTilesZ; Z++)
  {
          for(int X = -1; X <= numTilesX-1; X++)
          {
                  double X0 = vertices[a][0];
                  double Y0 = vertices[a][1];
                  double Z0 = vertices[a][2];
                  double Xi = X0 + ((40*X)/numTilesX);
                  double Zi = X0 + ((40*Z)/numTilesZ);
                  double Xf = Xi + 40/numTilesX;
                  double Zf = Zi - 40/numTilesZ;
                  glBegin(GL_POLYGON);                
                          //glColor3fv(colors[a]);
                          glNormal3fv(normals[a]);
                          glTexCoord2f(0.0, 0.0);        
                          glVertex3f(Xi,Y0,Zi);
                          glColor3fv(colors[b]);
                          glNormal3fv(normals[b]);
                          glTexCoord2f(1.0, 0.0);        
                          glVertex3f(Xf,Y0,Zi);
                          //glColor3fv(colors[c]);
                          glNormal3fv(normals[c]);
                          glTexCoord2f(1.0, 1.0);
                          glVertex3f(Xf,Y0,Zf);
                          //glColor3fv(colors[d]);
                          glNormal3fv(normals[d]);
                          glTexCoord2f(0.0, 1.0);                
                          glVertex3f(Xi,Y0,Zf);
                  glEnd();        
          }
  }        
}
glDisable(GL_TEXTURE_2D);
glFlush();

}

void buildGraphic()
{
// int a,b,c,d,e,tex
// a,b,c,d = vertices, colours and normals
// e = material colour
// tex = bitmap number
polygon(0,3,2,1,1,1); //Far Wall
polygon(2,3,7,6,2,1); //Roof
polygon(0,4,7,3,1,4); //Left wall
polygon(1,2,6,5,1,3); //Right wall
//polygon(4,5,6,7,5,1); //Near wall
//Doorway
  polygon(4,30,31,7,1,1);
  polygon(28,5,6,29,1,1);
//Floor
polygon(0,1,5,4,6,2);
//Main room
polygon(8,10,11,9,1,1); //Far wall
//polygon(12,14,15,13,1,1); //Near wall
//Doorway
  polygon(12,24,25,13,1,1);
  polygon(26,14,15,27,1,1);
//Floor
polygon(8,12,14,10,1,3);
//Ceiling
polygon(9,13,15,11,1,1);
//Bridge
polygon(30,28,24,26,1,5); //Floor
polygon(26,30,31,27,1,1); //Left wall
polygon(24,28,29,25,1,1); //Right wall
polygon(31,29,25,27,1,1); //Ceiling

}

void display(void)
{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//Update viewer position in modelview matrix
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(viewer[0],viewer[1],viewer[2], look_at[0], look_at[1], look_at[2], upX, upY, upZ);

//collisionDetection();

buildGraphic();
  
glutSwapBuffers();
glFlush();
}
The code above contains the display() function which sets up the view matrix and then calls
buildGraphic() function above it (the order of functions matters in C++, unlike C# which is fully
object-oriented). The buildGraphic() function calls the polygon() function a number of times - once per
wall, floor or ceiling constructed.

The polygon() function is not currently speed-optimised, it calls a bitmap from a resource file every time it
is required, but at least this is faster than calling it from a disk file. This function could be probably be
improved using 'texture binding' - but I will come back to that later.

The polygon function allows a tiled texture to be placed on the floor in the entrance room - I ought to
generalise the code to allow tiling on any surface, but at the moment this code is fine.

In order to load textures from a resource, i am using the MFC CBitmap function to create an image called
Image. We then use the CBitmap procedure GetBitmapBits to place the pixels into an array called
my_texels (each pixel needs 4 bytes, 3 for the RGB colour channels and one for the alpha-channel, so
the array must be 4 times larger than our bitmap is in bits). OpenGL requires bitmaps to have a width and
height which is a power of 2 (i.e. 1, 2, 4, 8, 16, 32, 64, 128, 512, 1024, ...) pixels.
Useful textures can save us a lot of geometry! Look out
through the porthole. Notice the thickness of the hull
surrounding the porthole - it's just an illusion created by the
texture - the wall is actually a thin sheet! Similarly the
edge-shading on the tile bitmap creates the impression of
raised tiles on the floor.
Again, the overhead lights appear to be set in a convincing
recess in the ceiling, but the ceiling is really flat!
void specialKeys(int specialkey, int x, int y)
{
double deltaX = look_at[0] - viewer[0];
double deltaZ = look_at[2] - viewer[2];
double newDeltaX, newDeltaZ;
double angle = 18;
double angleRad = (angle/360)*2*pi;

if(specialkey == GLUT_KEY_LEFT)
{
  //CCW rotation
  //Change delta                
  newDeltaX = deltaX*cos(angleRad) + deltaZ*sin(angleRad);
  newDeltaZ = -deltaX*sin(angleRad) + deltaZ*cos(angleRad);
  //Change look_at
  look_at[0] = viewer[0] + newDeltaX;
  look_at[2] = viewer[2] + newDeltaZ;
}
if(specialkey == GLUT_KEY_RIGHT)
{
  //CW rotation
  //Change delta
  newDeltaX = deltaX*cos(angleRad) - deltaZ*sin(angleRad);
  newDeltaZ = deltaX*sin(angleRad) + deltaZ*cos(angleRad);
  //Change look_at
  look_at[0] = viewer[0] + newDeltaX;
  look_at[2] = viewer[2] + newDeltaZ;
}
  
//Move in direction of look_at[]
if(specialkey == GLUT_KEY_PAGE_UP)
{              
  viewer[0] = look_at[0];
  viewer[2] = look_at[2];
  look_at[0] = look_at[0] + deltaX;
  look_at[2] = look_at[2] + deltaZ;    
}
if(specialkey == GLUT_KEY_PAGE_DOWN)
{              
  viewer[0] = viewer[0] - deltaX;
  viewer[2] = viewer[2] - deltaZ;
  look_at[0] = look_at[0] - deltaX;
  look_at[2] = look_at[2] - deltaZ;
}

if(specialkey == GLUT_KEY_UP)
{
  //viewer[1] += 1.0;
  look_at[1] += 1.0;
  if(look_at[1] >= 20)
  {
          look_at[1] = 20;
  }
}
if(specialkey == GLUT_KEY_DOWN)
{
  //viewer[1] -= 1.0;
  look_at[1] -= 1.0;
  if(look_at[1] <= -20)
  {
          look_at[1] = -20;
  }
}

display();
}

void myReshape(int w, int h)
{
glViewport(0, 0, w, h);

//Use a perspective view

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if(w<=h) glFrustum(-2.0, 2.0, -2.0 * (GLfloat) h/ (GLfloat) w,
2.0* (GLfloat) h / (GLfloat) w, dnear, dfar);
else glFrustum(-2.0, 2.0, -2.0 * (GLfloat) w/ (GLfloat) h,
2.0* (GLfloat) w / (GLfloat) h, dnear, dfar);

glMatrixMode(GL_MODELVIEW);
}

void Lights(void)
{
//Global ambience
GLfloat global_ambient[] = {2,0.2,0.2,1.0}; //A reddish light
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, global_ambient);

//Light source
GLfloat light1PosType[] = { 9.0, 9.0, 9.0, 1.0 };
GLfloat light2PosType[] = { -9.0, 9.0, -9.0, 1.0 };
GLfloat ambientColor[] = { 0.0, 0.0, 0.0, 1.0 };
GLfloat diffuseColor[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat specularColor[] = { 1.0, 1.0, 1.0, 1.0 };
//Light colours
glLightfv(GL_LIGHT1, GL_AMBIENT, ambientColor);
glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuseColor);
glLightfv(GL_LIGHT1, GL_SPECULAR, specularColor);
//
glLightfv(GL_LIGHT2, GL_AMBIENT, ambientColor);
glLightfv(GL_LIGHT2, GL_DIFFUSE, diffuseColor);
glLightfv(GL_LIGHT2, GL_SPECULAR, specularColor);
//Light attenuation
glLightf(GL_LIGHT1, GL_CONSTANT_ATTENUATION, 1.5-1);
glLightf(GL_LIGHT1, GL_LINEAR_ATTENUATION, 0.75-0.75);
glLightf(GL_LIGHT1, GL_QUADRATIC_ATTENUATION, 0.4+0.6);
//
glLightf(GL_LIGHT2, GL_CONSTANT_ATTENUATION, 1.5-1);
glLightf(GL_LIGHT2, GL_LINEAR_ATTENUATION, 0.75-0.75);
glLightf(GL_LIGHT2, GL_QUADRATIC_ATTENUATION, 0.4+0.6);
//Switch light on!
glEnable(GL_LIGHT1);
glEnable(GL_LIGHT2);
glEnable(GL_LIGHTING);
}
In the next chunk of code above, we invoke the specialkeys() function to receive input from certain 'special
keys' - namely the cursor keys and the Page-Up and Page-Down keys. (Use the keys() function to receive
input from 'normal' keys).

The reshape function takes care of any attempt by the user to resize the display window - this doesn't work
terribly well here and I need to study the reshape function more carefully. Lights() sets-up and switches on
the lights! In addition to specific point lights in precise locations, we have global ambience. Ambient lighting
gives some light to shaded objects (which in reality would be due to scattering and diffusion of light -
shadows are rarely totally black!), without ambience, the scene would be too dark, altering global ambience
is like altering the brightness setting of the image.

The final bit of code is the main() function, which is the main entry point of the program:
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;

// initialize MFC and print and error on failure
if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
{
  // TODO: change error code to suit your needs
  _tprintf(_T("Fatal Error: MFC initialization failed\n"));
  nRetCode = 1;
}
else
{
  // TODO: code your application's behavior here.
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
  glutInitWindowSize(600, 600);
  glutCreateWindow("Launch Bay");
  glutReshapeFunc(myReshape);
  Lights();
  glutDisplayFunc(display);
  glutSpecialFunc(specialKeys);
  glEnable(GL_DEPTH_TEST);

  glutMainLoop();
}

return nRetCode;
}
The format of this function was set-up for us by VS, because we have MFC support enabled rather than
an 'empty' project. The code we add is in the TODO sections, especially that in the else body. This code
creates the display window, gives it a specified size, calls the Lights() function to set-up the lighting,
associates display() with the OpenGL glutDisplayFunc() and myReshape with the glutReshapeFunc() and
tells the glutSpecialFunc() to monitor for input from special keys. Finally, glutmainLoop() which ends all
OpenGL programs, sets the program in a loop - the display is maintained in parallel to the monitoring of
input from the user.
Adding an object to launch from the launch bay!

I have modified the program so that a simple space-probe launches from the launch bay when you click
the right mouse button. Enter the bay area, right click and watch the probe accelerate away! This
modified version (v3) can be downloaded
3DLaunch_Bay_v3.

I added the following function above the buildGraphic function:
Still to do:

Add collision detection to stop the viewer walking through the walls!
Construct a more detailed probe geometry?
Add sound effects.
Make sure the code is optimised for speed.
void buildProbe()
{
GLUquadricObj * sphere2;
sphere2 = gluNewQuadric();
GLUquadricObj * cylinder_1;
cylinder_1 = gluNewQuadric();
gluQuadricDrawStyle(sphere2, GLU_FILL);
gluQuadricDrawStyle(cylinder_1, GLU_FILL);
glColor3f(0.0,1.0,1.0);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, colors[6]);

glPushMatrix();
   glTranslatef(probeTranslate,5,60);
   gluSphere (sphere2,5,20,20);
glPopMatrix();

glPushMatrix();
           glTranslatef(probeTranslate,5,60);
           glRotatef(45,1,0,0);
           gluCylinder (cylinder_1,2,0.1,10,10,10);
glPopMatrix();
glPushMatrix();
           glTranslatef(probeTranslate,5,60);
           glRotatef(-45,1,0,0);
           glRotatef(180,0,1,0);
           gluCylinder (cylinder_1,2,0.1,10,10,10);
glPopMatrix();
}
The function call:   buildProbe();

was also added to the buildGraphic() function.
The following function was added (just above the main function) to read input from the mouse:
void mouse(int btn, int state, int x, int y)
{
switch(btn)
{
case GLUT_RIGHT_BUTTON:
   if(state == GLUT_DOWN)
   {
           _beginthread( animate, 0, (void*)0 );
   }
   break;
case GLUT_LEFT_BUTTON:
   if(state == GLUT_DOWN)
   {
           //does nothing yet!
   }
   break;
default:
   break;
}
}
The following command was added to the main() function to alert OpenGL to monitor for mouse input:

          glutMouseFunc(mouse);

This function initiates a new thread that takes care of the animate() function, which I added just above the
mouse() function, below:
void animate(void * arg)
{
for(int i = 1; i <= 10000; i++)
{
   Sleep(100);
   probeTranslate = probeTranslate + 0.1*i;
   glutPostRedisplay();
}
}
This function and the buildProbe() function use the following global variable:

                                   float probeTranslate = -50;

Which sets the x-coordinate of the probe (initially at -50, in the right side of the launch bay).

This function sets up a loop to move the probe. The Sleep() function adds a pause, otherwise the thread
would execute too quickly for us to see the probe moving away! I have also made the probe accelerate as
it goes. Note that once the probe passes the far clipping plane (which I have now set to 200) it will not be
displayed.
Adding More Realism

I recently modified the code to make the probe launchable from a computer terminal - simply find the
terminal in the launch bay and then right-click your mouse and watch the probe launch! Furthermore,
there is no a planet visible from the launchbay port (as a textured sphere) toward which the probe is
headed. You can download this version below:

            
3DLaunch_Bay_v4

What this demonstration needs now are sound effects, at least 2D sound effects, but preferably 3D
(although if our character was operating in a vacuum then there would be no sound effects other than
their own breathing!).

I am also pondering whether or not to try and smooth the animation, perhaps by using a separate thread
to advance the image in a series of smaller steps for each key press rather than by one whole step at a
time (though as it is it does give me the impression of walking in space).
Collision Detection

Collision detection - detecting when objects, such as the viewer (camera) collide with the walls of a room,
or when two movers (animated objects) collide or when a shot hits a target is notoriously difficult and
accounts for the errors in many game test versions. Here we will look at one way of detecting collisions
between the viewer and the walls, to stop the viewer walking through the walls! [Click here to see an
example of collision detection in 2D].

We construct an imaginary sphere around the viewer and detect when this sphere attempts to move into
one of the walls (which would cause the two to overlap) and prevent movement in that direction (another
common option is to allow the viewer to slide along the walls). The method I used is shown below:
//First add the following two global variables

static int collisionWalls = 10; //Number of items i in array [i][6] below:
static double collisionHull[10][6] = {
     //Inner walls parallel to x-axis //Xmin,Xmax,-Dz,Nx,Ny,Nz
     {-20,20,20,0,0,1},{-60,60,-80,0,0,-1},{10,20,-20,0,0,-1},{-20,-10,-20,0,0,-1},
     {-60,-10,-40,0,0,1}, {10,60,-40,0,0,1},
     //Inner walls parallel to z-axis //Zmin,Zmax,-Dx,Nx,Ny,Nz
     {20,40,-10,-1,0,0}, {20,40,10,1,0,0}, {-20,20,-20,-1,0,0}, {-20,20,20,1,0,0}
};
//Add this global variable below the definitions for eyeX, eyeY and eyeZ
//This is  similar to the viewer[] array and will store the initial positions when the viewer is moving
static GLdouble view_buffer[] = {eyeX, eyeY, eyeZ};
//Modify the PAGE-UP and PAGE-DOWN key press functions in specialKeys() function
//to check for collisions before moving viewer by calling the collisionDetection() function

if(specialkey == GLUT_KEY_PAGE_UP)
{
    
    view_buffer[0] = look_at[0];
    view_buffer[2] = look_at[2];

    if(collisionDetection() == true)
    {
            return;
    }
    else
    {
            viewer[0] = view_buffer[0];
            viewer[2] = view_buffer[2];
            look_at[0] = look_at[0] + deltaX;
            look_at[2] = look_at[2] + deltaZ;
    }                
}
if(specialkey == GLUT_KEY_PAGE_DOWN)
{
    view_buffer[0] = viewer[0] - deltaX;
    view_buffer[2] = viewer[2] - deltaZ;
    
    if(collisionDetection() == true)
    {
            return;
    }
    else
    {
            viewer[0] = view_buffer[0];
            viewer[2] = view_buffer[2];
            look_at[0] = look_at[0] - deltaX;
            look_at[2] = look_at[2] - deltaZ;
    }
}
bool collisionDetection()
{
//return false; //Switch off

//Test against each wall as a polygon, e.g. (z = -20, from x = -20 to x = 20)
//Normal = (0,0,1), D = 20
//Viewer as sphere, radius = 2

double R = 2.0;
double distance, initDistance, min, max, Dx, Dz, Nx, Nz;
int index;
for(int i = 0; i <= collisionWalls-1; i++)
{
     min = collisionHull[i][0];
     max = collisionHull[i][1];
     Nx = collisionHull[i][3];
     Nz = collisionHull[i][5];
     index = 2*abs((int)Nx);
     
     if(view_buffer[0+index] < min || view_buffer[0+index] > max)
     {
             continue;
     }
     else if(view_buffer[0+index] > min && view_buffer[0+index] < max)
     {
             Dx = collisionHull[i][2] - Nx*R;
             Dz = collisionHull[i][2] - Nz*R;
             initDistance = Nz*(viewer[2] + Dz) + Nx*(viewer[0] + Dx);
             distance = Nz*(view_buffer[2] + Dz) + Nx*(view_buffer[0] + Dx);
             if(distance * initDistance <= 0)
             {
                     return true;
             }
             else
             {
                     continue;
             }
     }
}
return false;
}
At this stage I have only bothered with two types of wall - those aligned along the x-axis (such as the wall
with the control panel) and those aligned along the z-axis (such as the window to the star-cluster). I
have given the extent of each wall in its plane by stating the x-axis or z-axis endpoints (min and max)
and the distance of the wall from the coordinate origin (0,0,0) - where the viewer initially starts, along
the appropriate axis. For example, the viewer starts at 90,0,0) facing along the -z-axis and the first wall
you see is the first one given in the collisionHull array - it spans along the x-axis from x = -20 to x = 20
and is 20 units in front of the viewer's origin (at a distance Dz of z = -20) and the unit normal to the wall
(an arrow of length one pointing towards the origin) points in the +z-direction (0,0,1). This gives us the
information we need to detect collisions with this wall.
In the specialKeys() function, which detects our key presses, the collisionDetection() function is called
before movement occurs (otherwise we might detect a collision only after the viewer has hit the wall!
We are concerned here with the PAGE-UP key that can move the viewer forwards and the
PAGE-DOWN key that can move the viewer backwards. There is no need to worry about the other key
presses that simply rotate the viewer.
The collisionDetection() function first retrieves the necessary information from the collisionHull array,
checking each wall in turn to see if a collision has occurred and returning as soon as a collision is
detected. The index integer simply tells the function whether or not it is dealing with an x-aligned wall
or a z-aligned wall. The function then checks whether the viewer is positioned within the end-points of
the wall and only checks for a collision if the viewer is within the span of the wall. The function will then
check for a collision by calculating the distance (along the x-axis or z-axis) between the viewer
sphere's surface (at a radius 2 units around the viewer centre) and the plane of the wall. If the viewer
is trying to move through a wall, this distance will reverse sign, so that if the distance from the surface
multiplied by the initial distance from the surface will be negative (strictly <= 0).

Note that I have not set up collision detection for the outside walls - if you move out of the hanger into
space then you will be able to pass through a wall, until you hit the collision hull just on the inside the
wall. This is easily remedied by setting up more collision hulls - one more for each wall in the
collisionHull array for which the sign of the normal (Nx or Nz) is reversed. Reversing this sign simply
reverses which side of the wall the invisible collision hull is on - ideally we need one on each side.

Also, I have not placed collision detection fro the floor or ceiling - at the moment this is not required,
but if ramps, stairs and gravity were introduced it would be required - I will deal with this later. Also, i
have not allowed angled walls, again I intend to add that later. For now our collision detection works -
we can move around the map without walking through walls!

Download version will collision detection:
3DLaunchBay_v5
More OpenGL
Comment on this article!