Linux Goes 3D: An Introduction to Mesa/OpenGL

Jörg-Rüdiger Hill

Issue #31, November 1996

3D graphics aren't hard when you have an expert to tell you how it's done.

Recently, I installed Linux for the first time on my home computer. After the excitement of having a Unix workstation at home had faded away, I started looking for a way to port my molecular graphics program Viewmol to Linux. I used to work with IBM and Silicon Graphics workstations, and Viewmol had been written using the Silicon Graphics' Graphics Library (Iris GL). There are a lot of 3D graphics libraries available for Linux (over 180 are listed on the Technische Universitat Berlin web site, www.cs.tu-berlin.de/~ki/engines.html) including some rudimentary implementations of Iris GL (YGL at WWW.thp.Uni-Duisburg.DE/Ygl/ReadMe.html, 2D only, and VOGL at http://www.cs.kuleuven.ac.be/~philippe/vogl/), but none of the libraries had the full functionality that I needed—then I discovered Mesa. Mesa is a 3D graphics library which is source code compatible with OpenGL, Silicon Graphics' successor to Iris GL. Mesa's goal is to make programs which have been written for OpenGL runnable on every X windows system including Linux. So I took a better look at Mesa and decided to rewrite my program for OpenGL.

Mesa has been written mainly by Brian Paul over the last 3 years and is currently (as of this writing) at version 1.2.8. Nearly all of the OpenGL functionality is available; the only missing features are anti-aliasing, mip-mapping, polygon stippling and some of the texture querying functions. Mesa's home page, www.ssec.wisc.edu/~brianp/Mesa.html, lists a number of applications (basically scientific visualization tools, but also a VRML browser) which use it. Currently, Mesa can be called from C and Fortran routines.

While OpenGL has been designed as a software interface to high-performance (and high-price) graphics hardware, Mesa is a software-only solution which uses X windows to interface with the hardware. (Recently a SVGA driver and some support for 3D PC-hardware have been added to Mesa.) Therefore Mesa based programs usually execute slower than OpenGL based programs. Both libraries are hardware independent and window system independent; thus, the handling of the window system is left to the application programmer. Having the programmer handle windowing is different from Iris GL, but was considered necessary for Mesa in order to achieve hardware independence. OpenGL is the standard for 3D computer graphics and is managed by the Architecture Review Board. Implementations are available for a number of operating systems: different flavours of Unix, Windows and MacOS. Mesa also supports all these platforms. OpenGL requires an extension, GLX, in the X server to run. Mesa does not need this extension as it emulates the calls to GLX. There are commercial implementations of OpenGL available for Linux which also include X servers with GLX.

OpenGL/Mesa (I will use only the term Mesa in the following text, but it should be noted that everything applies to OpenGL as well) do not provide high-level commands for describing models in 3D. They do provide the necessary graphics primitives (e.g., points, lines, polygons) to build and manipulate models. Mesa provides the programmer with the ability to perform model building and manipulation completely in three dimensional space. All the details of converting the 3D model to a drawing on a flat screen are handled by the library, including one of the most tedious tasks in 3D programming—removal of hidden lines and surfaces. Mesa also offers “special effects” such as texture mapping, fog or blending.

Mesa's primary ftp site is iris.ssec.wisc.edu, but it can also be found at the usual places for Linux. Installation is easy—first unload the archive file using the command:

gzcat Mesa-1.2.8.tar.gz | tar xf -

then for a.out give the command:

make linux

or for ELF give:

make linux-elf

Executing make will compile the Mesa library, the GL utility library (GLU), the tk and auxiliary libraries, and a whole bunch of example programs. (Mesa's makefile comes configured for 46 different operating systems, including MS Windows.) I have found compilation to be hassle free on at least Linux, AIX, Irix and OSF1. The compiled libraries can be found in Mesa-1.2.8/lib and should be installed in either /usr/lib or /usr/local/lib. The header files (Mesa-1.2.8/include) should also be copied to either /usr/include or /usr/local/include. This step was not included in our make process—the following examples all assume that Mesa is installed in both /usr/local/lib and /usr/local/include.

The compilation produces a total of four libraries.

  • 1) libMesaGL.* contains all the basic graphics code.

  • 2) libMesaGLU.* provides some higher level functions, such as subroutines to draw geometric objects, splines etc.

  • 3) libMesaaux.* is an auxiliary library that is not really a part of Mesa. Since Mesa is window system independent, some simple window manipulation functions were needed. This library was created to demonstrate the features of OpenGL in the OpenGL Programming Guide (“The Red Book”). It is included with Mesa so that all the example programs from “The Red Book” which are included in the Mesa distribution can be compiled.

  • 4) libMesatk.* is another window system support library. libMesaaux.* relies on libMesatk.*, so to successfully link a program which uses libMesaaux.* -lMesatak must be added to the command line.

I don't wish to bore you with the usual “Hello, world” program. So since Mesa is a graphics library, we will start with something more appropriate to its function. Let's draw some geometric shapes:

#include<stdlib.h>
#include<GL/gl.h>
#include<glaux.h>
void display(void
{
  glClearColor(1.0, 1.0, 1.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3f(0.0, 0.0, 0.0);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);
  glBegin(GL_LINES);
  glVertex2f(-1.0, -1.0);
  glVertex2f(1.0, 1.0);
  glEnd();
  glBegin(GL_POLYGON);
  glVertex2f(0.25, -0.75);
  glVertex2f(0.75, -0.75);
  glVertex2f(0.75, -0.25);
  glVertex2f(0.25, -0.25);
  glEnd();
  glFlush();
}
void main(int argc, char **argv)
{
  auxInitDisplayMode(AUX_SINGLE | AUX_RGB);
  auxInitPosition(0, 0, 500, 500);
  auxInitWindow(argv[0]);
  auxMainLoop(display);
}

As you can see, there is a naming convention for all functions. All Mesa functions start with the letters gl. Functions in the auxiliary library start with the letters aux. The first two calls to the auxiliary library in main() specify the desired frame buffer configuration, i.e. single buffered, rgb mode. (There is also a double buffered configuration for animations and a colormap mode, but rgb mode is preferred and is easier to handle.) The third call opens a window, and the fourth call enters an infinite loop in which the function display() will be called whenever a redraw request is received from X windows. As I mentioned earlier, Mesa does not deal directly with the interface to the windowing system. The auxiliary library provides only the very basics and is not suited for larger programs—more about alternatives later.

The display() function starts with two calls to clear the background of the window to white—first we specify the desired color with glClearColor() and then we clear the color buffer with glClear(). Following that we set the drawing color to black and set up a projection matrix using glMatrixMode(), glLoadIdentity() and glOrtho(). Since Mesa can handle all the necessary mathematics to create a 2D drawing from our 3D world, we have only to give the instructions for making the projections. First we use glMatrixMode() to specify that we are going to manipulate the projection matrix. (There is a modeling matrix to translate or rotate objects that we discuss later.) Then we load an identity matrix to initialize the matrix stack, and finally, we use glOrtho() to specify an orthogonal projection. Now we draw a line from the lower left to the upper right corner of the window and a square in the lower right quadrant.

All drawing primitives in Mesa are created by embracing their vertex specifications with calls to glBegin() and glEnd(). In the call to glBegin() we specify the primitive we wish to draw. Available primitives are GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_POLYGON, GL_QUADS, GL_QUAD_STRIP, GL_TRIANGLES, GL_TRIANGLE_STRIP and GL_TRIANGLE_FAN.

Finally, we call glFlush() to tell Mesa to flush its graphics pipeline and display the objects we have specified. To compile our demo program (assume it has been stored under the name demo1.c), we execute the following command (note that the standard Xlib, the X extension library and the math library are needed to resolve all references from Mesa):

cc -o demo1 demo1.c -I/usr/local/include -L/usr/local/lib \
-lMesaaux -lMesatk -lMesaGL -lXext -lX11 -lm

Figure 1 shows the result of the execution of our program which is exited by pressing the <ESC> key. This exercise was definitely easier to program using the Mesa libraries than using X window directly.

Figure 1. Demo Program Output

Now, since Mesa is a library for 3D graphics, let's create a three dimensional object. Replace the display() function in our first example with the following calls:

 void display(void)
{
  glClearColor(1.0, 1.0, 1.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3f(0.0, 0.0, 0.0);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);
  glRotatef(45.0, 0.0, 1.0, 0.0);
  glRotatef(30.0, 0.0, 0.0, 1.0);
  auxWireCube(1.0);
  glFlush();
}

Recompile. Instead of using glBegin()/glEnd() pairs to specify the vertices for an object, we now use one of the auxiliary library functions to draw a wire frame cube. The two glRotatef() calls before the drawing change the model view matrix. Since rotations can only change the model view matrix, we are not required to switch to the model view matrix mode explicitly; the switch is made automatically by Mesa. The first call rotates the object 45 degrees about the Y axis, the second 30 degrees about the Z axis. The final letter, f, of glRotatef() indicates that its arguments are floating point. (Functions that end with the letter d, i or s accept arguments of type double, integer or short, respectively.) Internally, Mesa uses the float version of a function; thus, calling this version directly saves an additional function call within Mesa. Figure 2 shows the output generated by running this version of our program.

Figure 2. Wire Frame Cube

Next, we add interactivity to our program. To allow an interactive rotation of our cube, we have only to add some lines that deal with input:

#include<stdlib.h>
#include<GL/gl.h>
#include<glaux.h>
float xangle=0.0, yangle=0.0;
void display(void)
{
  glClearColor(1.0, 1.0, 1.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3f(0.0, 0.0, 0.0);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);
  glRotatef(xangle, 1.0, 0.0, 0.0);
  glRotatef(yangle, 0.0, 1.0, 0.0);
  auxWireCube(1.0);
  glFlush();
}
void rotX1(void)
{
  xangle+=5.;
}
void rotX2(void)
{
  xangle-=5.;
}
void rotY1(void)
{
  yangle+=5.;
}
void rotY2(void)
{
  yangle-=5.;
}
void main(int argc, char **argv)
{
  auxInitDisplayMode(AUX_SINGLE | AUX_RGB);
  auxInitPosition(0, 0, 500, 500);
  auxInitWindow(argv[0]);
  auxKeyFunc(AUX_LEFT, rotX1);
  auxKeyFunc(AUX_RIGHT, rotX2);
  auxKeyFunc(AUX_UP, rotY1);
  auxKeyFunc(AUX_DOWN, rotY2);
  auxMainLoop(display);
}

The display() function is nearly identical to the one in the previous example, we have exchanged the hard coded angle for a variable. Our main() function now includes four calls to auxKeyFunc() allowing us to specify a callback function that is called when a certain key is pressed (the constants used here refer to the cursor keys). Finally, we need functions that will increase or decrease the rotation angle of the cube depending on which key is pressed. The program is again compiled in the same manner. When this version of our program is running, the cube can be rotated by pressing any of the cursor keys.

Sidebar: Messa/OpenGL Resources

We would probably prefer our application to use the mouse to rotate the cube, but we are currently limited to the functions provided by the auxiliary library. To undertake writing the program to use mouse clicks instead of cursor keys would require us to use one of the more sophisticated X windows interfaces (but thats another article).

Finally, we will add some light effects to our cube demo and show how to remove hidden surfaces. These calculations are also easily handled by calls to Mesa, and the programmer does not have to worry about the underlying, non-trivial mathematics. We again modify the display() function as follows:

void display(void)
{
  GLfloat light0[4] = {0.5, 0.8, 1.0, 0.0};
  GLfloat color[4]  = {1.0, 0.0, 0.0, 0.0};
  glClearColor(1.0, 1.0, 1.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glLightfv(GL_LIGHT0, GL_POSITION, light0);
  glRotatef(xangle, 1.0, 0.0, 0.0);
  glRotatef(yangle, 0.0, 1.0, 0.0);
  auxSolidCube(1.0);
  glFlush();
}

Since drawing a wire frame cube with lighting enabled does not make much sense, we will use a solid cube. For a solid cube to be rendered correctly we need to remove hidden surfaces. In Mesa this can be accomplished by using the z-buffer which stores information about the depth value of a point in 3D space. Mesa will then automatically only draw pixels which are visible. To initialize the z-buffer prior to the drawing we just add the constant GL_DEPTH_BUFFER_BIT to the call to glClear().

For lighting calculations we cannot simply use a drawing color—we have to link a color to an object. Mesa uses “materials” to make this link and allows us to specify the properties of a material. The call to glMaterialfv() assigns red as the color for diffuse reflections to both the front and back sides of all polygons. We specify the position of the light with a call to glLightfv(). Mesa can use a number of different lights (at least 8 are guaranteed), and the constants GL_LIGHT0 ... GL_LIGHT7 can be used to reference them. GL_POSITION informs Mesa that we are specifying a position (other possibilities include light and color), and the vector light0[] places the light on the specified axis at infinite distance. This particular axis is used to achieve different light intensities on the different faces of the cube. Notice that these two functions show another type of naming convention—both names end with the letters fv, i.e., the arguments are vectors of floating point values.

We also need to modify the main() function to include z-buffering and lighting calculations:

void main(int argc, char **argv)
{
  auxInitDisplayMode(AUX_SINGLE | AUX_RGB | AUX_DEPTH);
  auxInitPosition(0, 0, 500, 500);
  auxInitWindow(argv[0]);
  auxKeyFunc(AUX_LEFT, rotX1);
  auxKeyFunc(AUX_RIGHT, rotX2);
  auxKeyFunc(AUX_UP, rotY1);
  auxKeyFunc(AUX_DOWN, rotY2);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  auxMainLoop(display);
}

First the constant AUX_DEPTH in the call to auxInitDisplayMode() instructs X windows to provide a window with a z-buffer. We then use smooth shading (glShadeModel()) to draw polygons that have varying color over the face of the polygon. If we used flat shading (the default), the different polygons would be clearly visible. Of course, that would not make a difference in the case of a cube, but would with other objects (e.g., replace the cube with a cone using auxSolidCone(1.0, 1.0) and see the result). Finally, we enable lighting calculations, light0 and depth testing using calls to glEnable(). For depth testing we specify a function to compare the depth values so that only smaller values, i.e., closer to the viewer, are considered. Recompile. The lit, rotatable cube shown in Figure 3 is the output of our program after some rotations have been done.

Figure 3. Rotatable Cube

We have now covered the basic drawing operations to produce realistic 3D scenes using Mesa. The auxiliary library used in these examples is insufficient as an interface to the window system for larger scale programs. One alternative is to use the GL Utility Toolkit (GLUT) that transparently provides the same functionality as Iris GL (e.g., window and event handling, menus). GLUT was written by Mark Kilgard at Silicon Graphics and is available free. Another option is to use OpenGL widgets that are provided with the Mesa package in the widgets subdirectory. (This subdirectory must be compiled separately.) A program could then do all the windows and events handling in the normal X fashion and create one or more OpenGL widgets to display 3D graphics. Drawing into these widgets can be accomplished using calls to Mesa. As a final example of what Mesa can do, Figure 4 shows a rendering of a molecular orbital of benzene using my molecular graphics program Viewmol. (The OpenGL/Mesa version of Viewmol has not been released yet, but will appear at the same locations where the Iris GL version can be found today: ftp://ccl.osc.edu/pub/chemistry/software/SOURCES/C/viewmolorftp://ftp.ask.uni-karlsruhe.de/pub/education/chemistry/viewmol_ask.html)

Figure 4.Molecular Orbital of Benzene

Jörg-Rüdiger Hill (jxh@msi.com) was born in Berlin, Germany and holds a Ph.D. in theoretical chemistry. He works for a molecular modeling software company and is currently porting his molecular graphics program Viewmol to Linux. He has been running Linux since version 1.0.9. He much prefers the Southern Californian weather over that in Berlin.