Modeling Your Own 3D Objects

In previous projects we have used solid and wire-frame geometric objects provided by the GLUT.  Sometimes we will want to build 3D objects of our own.  Our first example will be a cylinder comprised of a strip of rectangles (quads).  In the sample code below we have generated a wire-frame representqion of rectangles to emphasize the placement of vertex points around the circular cylinder.
  

As we review these example programs it is important to become familiar with the use of spherical coordinates.  A point in spherical coordiates is defined by the radius (distance from the orgin), an azimuthal angle ( - angle in the x,z plane from the positive x axis) and the elevation angle ( - angle from the negative y axis).


// wire_frame demo

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

  GLfloat yang = 0.0; 
  GLfloat xang = 0.0; 
  double const pi = 3.1415926;

 void display() 
  { 
  int ang;
  int delang = 10;
  float y0 = 0.0;
  float y1 = 0.3;
  float r0 = 0.4;
  float x,z,xold,zold;

     glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); 
     glLoadIdentity(); 
     glRotatef(yang,1.0,1.0,0.0); 
     glTranslatef(-0.4,0.25,0.0); 
     glRotatef(xang,0.0,0.0,1.0); 
     glBegin(GL_LINES); 
        glColor3f(0.7,0.7,0.7); 
        glVertex3f(r0,y0,0.0);
        glVertex3f(r0,y1,0.0);
        xold=r0;
        zold=0.0;
       for(ang=delang;ang<=360;ang+=delang)
        {
          x=r0*cos((double)ang*2.0*pi/360.0);
          z=r0*sin((double)ang*2.0*pi/360.0);
          glVertex3f(x,y0,z);
          glVertex3f(x,y1,z);
          glVertex3f(xold,y0,zold);
          glVertex3f(x,y0,z);
          glVertex3f(xold,y1,zold);
          glVertex3f(x,y1,z);
          xold=x;
          zold=z;
        }
     glEnd(); 
     yang = yang + 1.2; 
    if (yang>360.0) 
       yang = 0.0; 
     xang = xang - 0.5; 
    if (xang<0.0) 
       xang = 360.0; 
     glutSwapBuffers(); 
     glFlush(); 
  } 

  void init() 
  { 
     glClearColor(1.0,1.0,1.0,1.0); 
     glClearDepth(1.0); 
     glEnable(GL_DEPTH_TEST); 
     glMatrixMode(GL_PROJECTION); 
  } 
 

  void main(int argc, char** argv) 
  { 
     glutInit(&argc,argv); 
     glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); 
     glutInitWindowSize(400,400); 
     glutInitWindowPosition(0,0); 
     glutCreateWindow("3D Rotation"); 
     glutDisplayFunc(display); 
     glutIdleFunc(display); 
     init(); 
     glutMainLoop(); 
  }
 

The code above generates an animated wireframe of a circular cylinder as shown below.  Notice that each rectangle is drawn using four straight lines.  In our wire-frame display, however, each rectangle shares an edge with its neighbors.  Therefore we do no have to draw these lines for both rectangles.  In the main  loop in the code above only three lines are drawn in each pass.  These three lines are in a "C shape".  The "C's" connect one to the next to make up the cylinder.

In this sample code the original points are defined for a circle of radius 0.4 centered on the origin and in the x-z plane.  The cylinder is defined by two circles of points in the x-z plane a y position y0=0.0 and y1=0.3.  Rectangles making up the cylinder are defined by the four vertices
(xold,y0,zold)
(xold,y1,zold)
(x,y0,z)
(x,y1,z)
where x and z are converted from spherical coordinates (r0 = radius, ang = angle)  into Cartesian coordinates (x, y).  
x=r0*cos((double)ang*2.0*pi/360.0)

z=r0*sin((double)ang*2.0*pi/360.0)

and ang is incremented from 0 through 360 degrees.  The value of xold and zold are the values of x and z from the previous iteration.

Small modifcations to the point generator can produce shapes that are not radially symmetric (uhmm...not round).  For example, changing the expression for x above to,

 x=(r0+r0/2.0*sin((double)ang*2.0*pi/360.0))*cos((double)ang*2.0*pi/360.0);
yields,

Sometimes we will also want to vary the radius of the object as a function of the height above the x-z plane.  In the next example we will generate a wire-frame sphere of radius r0.

The general expressions for the conversion from sperical to cartesian coortinates are listed below.  A point P on a sphere of radius r centered on the origin is defined by the radial angle q and the polar angle f. The point P is transformed to cartesian coordinates (x,y,z) by the trigonometric relations,

from Geometry Formulas and Facts by Silvio Levy
// wire_frame demo

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

  GLfloat yang = 0.0; 
  GLfloat xang = 0.0; 
  double const pi = 3.1415926;

 void display() 
  { 
  int vang,ang;
  int delang = 10;
  float r0 = 0.4;
  float x0,y0,z0,x1,y1,z1,x2,z2;

     glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); 
     glLoadIdentity(); 
     glRotatef(yang,1.0,1.0,0.0); 
     glTranslatef(-0.4,0.25,0.0); 
     glRotatef(xang,0.0,0.0,1.0); 
     glBegin(GL_LINES); 

  for(vang=0;vang<=180;vang+=delang)
  {
     y0=r0*cos((double)(vang)*2.0*pi/360.0);
     y1=r0*cos((double)(vang+delang)*2.0*pi/360.0);
     x0=r0*sin((double)vang*2.0*pi/360.0);
     z0=0.0;
     for (ang=0;ang<=360;ang+=delang)
     {
        x1=r0*cos((double)ang*2.0*pi/360.0)*sin((double)vang*2.0*pi/360.0);
        x2=r0*cos((double)ang*2.0*pi/360.0)*sin((double)(vang+delang)*2.0*pi/360.0);
        z1=r0*sin((double)ang*2.0*pi/360.0)*sin((double)vang*2.0*pi/360.0);
        z2=r0*sin((double)ang*2.0*pi/360.0)*sin((double)(vang+delang)*2.0*pi/360.0);
        glColor3f((r0-x0)/r0,(r0-y0)/r0,(r0-z0)/r0);         
        glVertex3f(x0,y0,z0);
        glVertex3f(x1,y0,z1);
        glVertex3f(x1,y0,z1);
        glVertex3f(x2,y1,z2);
        x0=x1;
        z0=z1;
     }
  }
     glEnd(); 
     yang = yang + 1.2; 
     if (yang>360.0) 
      yang = 0.0; 
     xang = xang - 0.5; 
     if (xang<0.0) 
      xang = 360.0; 
     glutSwapBuffers(); 
     glFlush(); 
  } 

  void init() 
  { 
     glClearColor(0.0,0.0,0.2,1.0); 
     glClearDepth(1.0); 
     glEnable(GL_DEPTH_TEST); 
     glMatrixMode(GL_PROJECTION); 
  } 
 
  void main(int argc, char** argv) 
  { 
     glutInit(&argc,argv); 
     glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); 
     glutInitWindowSize(400,400); 
     glutInitWindowPosition(0,0); 
     glutCreateWindow("3D Rotation"); 
     glutDisplayFunc(display); 
     glutIdleFunc(display); 
     init(); 
     glutMainLoop(); 
  }

Some modifications of our previous shell program have been made for asthetic reasons.  Note that the expressions converting from spherical to Cartesian coordiates assume that the y-z plane is the viewing plane, while openGL defaults to the x-y plane as the viewing plane.  This is because openGL renders 3D objects as a generalization of 2D objects in which the drawing surface is labeled as the x-y plane.  Consider the exchange of coordinate labels as an entertaining exercise in mental gymnastics.

Surfaces of Revolution
(You say you want a revolution?)

Most of the time we will not be able to generate an analytic expression for the shape of the object we want to model.  If the object is radially symmetric we can use a pair of arrays or an array of a structured type to hold the r,y values representing the contour of the object surface which will be revolved around the axis of symmetry (the y axis in this case) using cylindrical (polar) coordinates. Consider the contour shown below:

These 12 points define the contour of the object to be generated.  The first values  in each data pair is the distance from the y axis parallel to the x axis, which is the radius of the shape at each point.  The height of each point from the x-z axis (the y value) is the second parameter in the data pair.  These can be used to compute the x and z values to compute points on the surface of the objects.

  float r[12] = {0.0,0.3,0.3,0.25,0.25,0.35,0.35,0.3,0.15,0.15,0.05,0.0};
  float y[12] = {0.0,0.0,0.05,0.1,0.3,0.35,0.45,0.5,0.5,0.4,0.3,0.3};
These points can be used to render either a wire frame or a solid three-dimensional model of the object.

Each segment occupies 10 degrees so there are 36 segments in one revolution.  Changing the radial angle (delang) in this program  from 10 to 60, 90 or 120 results in an interesting effect.




The points shown above are used to represent the radius r and the height y of a ring of points parallel to the x-z plane.  These rings of points are used to draw the lines of the wireframe as shown below.

<>

The profile points are rotated around the y axis in a for_loop with delang as the change in the radial angle per pass through the loop.  For each pass the points p1, p2 and p3 are used to draw two lines (shown in red).  Since the profile is defined as (r,y) pairs we can derive the x and z values for each point using the following transformations,
x1=r[i]*cos((double)ang*2.0*pi/360.0);
z1=r[i]*sin((double)ang*2.0*pi/360.0);
x2=r[i+1]*cos((double)ang*2.0*pi/360.0);
z2=r[i+1]*sin((double)ang*2.0*pi/360.0);
x3=r[i]*cos((double)(ang+delang)*2.0*pi/360.0);
z3=r[i]*sin((double)(ang+delang)*2.0*pi/360.0);
The corresponding y[ ] values are used directly from the profile array as the y coordinates to generate the pair of lines.  For the ith pass through the loop, p1 and p3 are at height y[i] while p2 is at height y[i+1].
glVertex3f(x1,y[i],z1);   // this is line p1--p2
glVertex3f(x2,y[i+1],z2);

glVertex3f(x1,y[i],z1);   // this is line p1--p3
glVertex3f(x3,y[i],z3);

Using delang=10 gives us the wireframe representation of our 3D object as shown below.

We will use this object model as our sample object.  The first step is to replace the wireframe with solid facets.  This can be most easily accomplished using the GL_QUAD_STRIP drawing mode.
 
  int ang,i; 
  int delang = 10; 
  float r[12] = {0.0,0.3,0.3,0.25,0.25,0.35,0.35,0.3,0.15,0.15,0.05,0.0};
  float y[12] = {0.0,0.0,0.05,0.1,0.3,0.35,0.45,0.5,0.5,0.4,0.3,0.3};

  float x1,x2,z1,z2,x3,z3;
  for (i=0;i<11;i++)
  {
  glBegin(GL_QUAD_STRIP);
  for(ang=0;ang<=360;ang+=delang) 
  { 
   x1=r[i]*cos((double)ang*2.0*pi/360.0);
   x2=r[i+1]*cos((double)ang*2.0*pi/360.0);
   z1=r[i]*sin((double)ang*2.0*pi/360.0); 
   z2=r[i+1]*sin((double)ang*2.0*pi/360.0); 
   x3=r[i]*cos((double)(ang+delang)*2.0*pi/360.0);
   z3=r[i]*sin((double)(ang+delang)*2.0*pi/360.0);
   glColor3f((1.0+cos((double)ang*pi*2.0/360))/2.1,
             (1.0+cos((double)ang*pi*2.0/360))/2.1,
             (1.0+cos((double)ang*pi*2.0/360))/2.1);
   glVertex3f(x1,y[i],z1); 
   glVertex3f(x2,y[i+1],z2);
   };
 glEnd(); 
 }

This modification produces the following image.

Note that the shading effect is accomplished in this example by making the color a function of the radial angle.  This produces an interesting, though not very realistic effect.  We need to make the color, and surface properties a function of the surface material.  We also need to make the lighting and shading a function of a light source at a specific location.

We can specify a light source position and extent using the glLightfv( ) function.  The array light_pos[ ] gives the position in 3-space and the extent (size) of the source.  Setting the fourth parameter to 0.0 makes the light a distant source.  This means that all the rays of light are passing through the viewing volume parallel.
 
void init() 

  float light_pos[] = {2.0, 1.0, 1.0, 10.0};
  float ambient[4] = {0.5, 0.5, 0.5, 1.0};
  float diffuse[4] = {0.5, 0.5, 0.5, 1.0};

  glClearColor(1.0,1.0,1.0,1.0); 
  glClearDepth(1.0); 
  glLoadIdentity();
  glLightfv(GL_LIGHT0, GL_POSITION, light_pos);
  glEnable(GL_DEPTH_TEST); 
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glShadeModel(GL_SMOOTH);
  glMatrixMode(GL_MODELVIEW);
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
  glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);
  glMatrixMode(GL_PROJECTION); 

We have also set the surface properties of the facets with glMaterialfv( ), which is the vector form of the material property function.  For now we have specified the ambient and diffuse components of the surface for both the front and the back of each facet.  These values will remain valid until they are changed with another call to glMaterialfv( ).

If you are thinking that there appears to be a problem, you are correct.  The problem is that the surface is much too dark and the lighting and shading doesn't seem to be consistent.  For example, the vertical surface around the bottom of the object on the left side is lighter than the vertical surface on the same portion of the upper part of the object.  Also, since the light source is higher than the object, its top should be illuminated.

Now for the bad news.  The openGL function glMaterialfv( ) requires that the unit normal vectors on each surface be computed and specified using the glNormal3fv( ) before the corresponding quad (or triangle) is defined in a glBegin( )..glEnd( ) construct.

To calculate the normal to a flat surface we start with three points on the surface that are not collinear (not all in a straight line) say p1, p2 and p3 as shown below.



Taking the difference between p2 and p1 gives us a vector v1 that is lying in the plane of the surface.  Taking the difference between p3 and p1 gives us another vector v2 that is lying in the plane of the surface, is not parallel with v1 and has same starting point as v1.  The normal at the point p1 is then computed by taking the vector cross product of v1 into v2.  Dividing each component of this normal vector by its magnitude produces the unit normal to the point p1 which is also normal (i.e. perpendicular) to the surface containing the points p1, p2 and p3 and facing outward.

The difference calculations can be incorporated into the body of the quad rendering loop of our display( ) function.  The computations of unit normals can be specified and called as separate functions.
 
 void normalize(float v[3]) 

    float d = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]); 
    if (d != 0.0)
    { 
       v[0]/=d;
       v[1]/=d; 
       v[2]/=d; 
    } 

void normCrossProd(float v1[3], float v2[3], float out[3]) 

   out[0] = v1[1]*v2[2] - v1[2]*v2[1]; 
   out[1] = v1[2]*v2[0] - v1[0]*v2[2]; 
   out[2] = v1[0]*v2[1] - v1[1]*v2[0]; 
   normalize(out); 


for (i=0;i<11;i++)
{
 glBegin(GL_LINES);
  for(ang=0;ang<=360;ang+=delang) 
  { 
   x1=r[i]*cos((double)ang*2.0*pi/360.0);
   x2=r[i+1]*cos((double)ang*2.0*pi/360.0);
   z1=r[i]*sin((double)ang*2.0*pi/360.0); 
   z2=r[i+1]*sin((double)ang*2.0*pi/360.0); 
   x3=r[i]*cos((double)(ang+delang)*2.0*pi/360.0);
   z3=r[i]*sin((double)(ang+delang)*2.0*pi/360.0);

   v1[0]=x2-x1;                    // difference between p2 and p1
   v1[1]=y[i+1]-y[i];
   v1[2]=z2-z1;

   v2[0]=x3-x1;                    // difference between p3 and p1
   v2[1]=y[i+1]-y[i];
   v2[2]=z3-z1;

   normCrossProd(v1,v2,normal);    // normalized (unit) cross product
   glNormal3fv(normal); 
   glVertex3f(x1,y[i],z1); 
   glVertex3f(x2,y[i+1],z2);
   glVertex3f(x1,y[i],z1);
   glVertex3f(x3,y[i],z3);
   };
 glEnd(); 
}

The image below shows our 3D object with surface normal vectors overlayed (red dots indicate the end of the vectors).

Now that we are computing the unit normals for each facet, we can take advantage of openGL's surface material property rendering capabilities.  Using the vector form of glMaterial3fv( ) we can specify the ambient, diffuse and specular properties of the surfaces in each of the primary colors (red, green and blue) as well as the general shininess.  We will apply our textbook's parameter set for defining metallic gold or brass materials to the surfaces of our sample 3D object.
 
void init() 

  // material parameter set for metallic gold or brass
  float light_pos[] = {2.0, -1.0, 1.0, 10.0};
  float ambient[4] = {0.33, 0.22, 0.03, 1.0};
  float diffuse[4] = {0.78, 0.57, 0.11, 1.0};
  float specular[4] = {0.99, 0.91, 0.81, 1.0};
  float shininess = 27.8;

  glClearColor(1.0,1.0,0.95,1.0); 
  glClearDepth(1.0); 
  glLoadIdentity();
  glLightfv(GL_LIGHT0, GL_POSITION, light_pos);
  glEnable(GL_DEPTH_TEST); 
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glShadeModel(GL_SMOOTH);
  glMatrixMode(GL_MODELVIEW);
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
  glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, shininess);
  glMatrixMode(GL_PROJECTION); 




References

The OpenGL Utility Toolkit (GLUT) Programming Interface API Version 3 by Mark J. Kilgard of SGI.

The OpenGL WWW Man-Pages - mirror at Tulane University