Drawing and Printing OpenGL Graphics Using Device-Independent Bitmaps

Environment: VC++6, Win32

Overview

The OpenGL API has been available to PC programmers who wanted to display rendered 3D graphics since Windows NT 3.51 came out in the mid-1990s. It's a terrific platform for 3D visualization for two primary reasons: 1) it exposes some very advanced rendering features, and 2) it is very easy to use.

You might be thinking: "Easy to use???" Well, it is, and this article is intended to be a short primer for someone new to OpenGL programming on the PC.

Normally, a window receives the output from the OpenGL library, using a double-buffer approach. This article shows how to send the OpenGL graphics output directly to a DIB (device-independent bitmap) section.

The decision on whether to draw to a DIB or a window depends on your application; however, drawing straight to a DIB has several convenient advantages: mouse selection of 3D objects is simplified, printing high resolution graphics becomes trivial, the DIB can be displayed on any window (dialog, view, and so forth), and it allows you to use image processing techniques after OpenGL renders the 3D image.

Look and you will find an infinite number of articles and books on how to do 3D programming, with and without graphics libraries such as OpenGL, and how to optimize your 3D code for speed. This article takes a slightly different track and assumes you have some idea of how to do 3D programming and you want to learn to use the OpenGL API effectively.

The following steps allow you to render 3D graphics to a DIB:

  1. Create a DIB section. It can't be a regular bitmap; OpenGL requires that it be a DIB Section.
  2. Initialize the OpenGL library and connect the library with the new DIB.
  3. Execute the drawing code.
  4. Render the DIB to the screen or to the printer.

That's it. You're drawing 3D graphics directly into an image!

Creating a DIB Section

A simple call to CreateDIBSection sets up the image for drawing. In the sample project, the DIB section is encapsulated within the DIBSection class to make things a little easier.

Initialize the OpenGL library and connect the library with the new DIB

The OpenGL API is a C-style interface that operates like a state engine. Think of it like a car. You start it up, and then you put it in gear and give it commands. When you issue commands to the OpenGL engine, its state determines how those commands are implemented. Using the car analogy, what happens when you step on the gas depends very much on whether you are in first gear or reverse.

For initialization, call the following OpenGL functions: SetPixelFormat, wglCreateContext, wglMakeCurrent. The key for drawing directly to a DIB is the SetPixelFormat call: for the library to play nice with a DIB section you have to set the pixel descriptor flags to the following:

PFD_DRAW_TO_BITMAP | PFD_SUPPORT_OPENGL |PFD_SUPPORT_GDI

Next, make the following calls to set up how you want your 3D model to look: glViewport, glMatrixMode, glPolygonMode, and glShadeModel.

Now OpenGL should be ready to go.

Execute the drawing code:

This section is intended to give you a quick idea of how to use the drawing calls. Complex drawings are typically built from lots of "primitive" drawing commands. For example, if you want to draw the surface of a rectangular grid, you may want to break the surface in to (flat) triangles. To accomplish this, you have to first tell the library that you are sending it triangle vertices, and when you are finished you call glEnd to tell the library you are no longer sending vertices:

glBegin(GL_TRIANGLES);
glDouble v1[3];
glDouble v2[3];
glDouble v3[3];
glDouble x, y, z;

for (int row = 0; row < (GRID_ROWS-1); row++){
  for (int col = 0; col < (GRID_COLUMNS-1); col++){
    // get some x, y, and z values

    x = (glDouble)row;
    y = (glDouble)col;
    z = (glDouble)((row%2) + (col%3));
    v1[0] = x; v1[2] = y; v1[1] = z;

    glVertex3dv(v1);
    z = (glDouble)((row%3) + (col%2));
    v2[0] = x; v2[2] = y + 1; v2[1] = z;

    glVertex3dv(v2);

    z = (glDouble)((row%4) + (col%3));
    v3[0] = x+1; v3[2] = y+1; v3[1] = z;

    glVertex3dv(v3);

    z = (glDouble)((row%3) + (col%2));
    v1[0] = x; v1[2] = y; v1[1] = z;

    glVertex3dv(v1);
    z = (glDouble)((row%3) + (col%4));
    v2[0] = x + 1; v2[2] = y; v2[1] = z;

    glVertex3dv(v2);
    z = (glDouble)((row%2) + (col%3));
    v3[0] = x+1; v3[2] = y+1; v3[1] = z;

    glVertex3dv(v3);

  }

}

glEnd();

The same approach is used for drawing lines, for example:

glBegin(GL_LINES);
// send the API pairs of vertices by calling glVertex3dv(v)
// as many times as necessary
glEnd();

A slightly different approach is necessary for drawing polygons because there is no way for the API to know ahead of time how many sides the polygons are going to have. So, you have to nest the glBegin(GL_POLYGON) and glEnd() within the polygon loop.

Also, the API uses a naming convention for its function calls that helps programmers keep the parameters straight. For example, the call to glVertex3dv actually has many variations:

  • glVertex3fv—passing in 3 glFloat types
  • glVertex3di—passing in 3 glInt types
  • glVertex3dv—passing in 3 glDouble types...and so on

Render the DIB to the screen or to the printer:

Create a CClientDC (MFC) for the window you wish to show the graphics in and transfer the image to the window using bitblt. In the sample project, this is accomplished by using the DIBSection object.

The following code snippet (from the sample project) shows some of the implementation of the OpenGL view class.

GLView3D::GLView3D(void)
{
  InitImage();
}

GLView3D::~GLView3D(void)
{
}

void GLView3D::CreateImage(UINT32 width, UINT32 height)
{
  if ((width > 0) && (height > 0))
  {
    m_dib.Create(width,height,32);
    if (!IsInitialized())
    {
      InitializeOpenGL();
      if (CreateView())
      {
        m_IsInitialized = 1;
      }
    }
  }
}

void GLView3D::SizeImage(UINT32 width, UINT32 height)
{
  if ((width > 0) && (height > 0))
  {
    if (!IsInitialized())
    {
      CreateImage(width,height);
    }
    else
    {
      if (!m_dib.IsCreated())
      {
        m_dib.Create(width,height,32);

        if (SetImagePixelFormat(*m_dib.GetDC()))
        {
          CreateImageGLContext(*m_dib.GetDC());
        }
      }
      else if ((width != m_dib.Width()) ||
               (height != m_dib.Height()))
      {
        m_dib.Create(width,height,32);
        CreateView();
      }
    }

    glViewport(0,0,width,height);
    double aspect;
    aspect = (height == 0) ? (double)width :
             (double)width/(double)height;
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45,aspect,0.01,25.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
  }
}

void GLView3D::InitializeOpenGL(void)
{
  m_hGLContext = NULL;
  m_GLPixelIndex = 0;
  InitGeometry();
}

void GLView3D::InitGeometry(void)
{
  m_model_matrix.SetRotation(45.0,0.0,0.0);
  m_model_matrix.SetScale(1.0,1.0,1.0);
  m_model_matrix.SetPosition(0.0,0.0,-5.0);
}


// Lights, camera, action...
int GLView3D::CreateView(void)
{
  int result = 0;

  // do the standard OpenGL initialization
  if(SetImagePixelFormat(*m_dib.GetDC()) &&
     CreateImageGLContext(*m_dib.GetDC()))
    result = 1;
  else
    return result;

  glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_NORMALIZE);
  glEnable(GL_COLOR_MATERIAL);

  // Lights, material properties
  GLfloat ambientProperties[] = {0.3f, 0.3f, 0.3f, 0.7f};
  GLfloat diffuseProperties[] = {0.9f, 0.9f, 0.9f, 1.0f};
  GLfloat specularProperties[] = {0.7f, 0.7f, 0.7f, 1.0f};
  GLfloat mat_amb_diff[] = { 0.7f, 0.7f, 0.7f, 1.0f};
  GLfloat shine[] = { 128.0f};
  GLfloat emissionProperties[] = {0.2f, 0.2f, 0.2f, 1.0f};
   
  glLightfv( GL_LIGHT0, GL_AMBIENT, ambientProperties);
  glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuseProperties);
  glLightfv( GL_LIGHT0, GL_SPECULAR, specularProperties);

  glLightModelf(GL_LIGHT_MODEL_TWO_SIDE, 1.0);

  // lighting
  glEnable(GL_LIGHT0);
  glEnable(GL_LIGHTING);
  glEnable(GL_DEPTH_TEST);

  return result;
}

BOOL GLView3D::SetImagePixelFormat(HDC hDC)
{
  PIXELFORMATDESCRIPTOR pixelDesc;

  pixelDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR);
  pixelDesc.nVersion = 1;

  pixelDesc.dwFlags = PFD_DRAW_TO_BITMAP | PFD_SUPPORT_OPENGL |
    PFD_SUPPORT_GDI;

  pixelDesc.iPixelType = PFD_TYPE_RGBA;
  pixelDesc.cColorBits = 32;
  pixelDesc.cRedBits = 0;
  pixelDesc.cRedShift = 0;
  pixelDesc.cGreenBits = 0;
  pixelDesc.cGreenShift = 0;
  pixelDesc.cBlueBits = 0;
  pixelDesc.cBlueShift = 0;
  pixelDesc.cAlphaBits = 0;
  pixelDesc.cAlphaShift = 0;
  pixelDesc.cAccumBits = 0;
  pixelDesc.cAccumRedBits = 0;
  pixelDesc.cAccumGreenBits = 0;
  pixelDesc.cAccumBlueBits = 0;
  pixelDesc.cAccumAlphaBits = 0;
  pixelDesc.cDepthBits = 32;
  pixelDesc.cStencilBits = 0;
  pixelDesc.cAuxBuffers = 0;
  pixelDesc.iLayerType = PFD_MAIN_PLANE;
  pixelDesc.bReserved = 0;
  pixelDesc.dwLayerMask = 0;
  pixelDesc.dwVisibleMask = 0;
  pixelDesc.dwDamageMask = 0;

  int pix_index = ChoosePixelFormat(hDC,&pixelDesc);

  if(!SetPixelFormat(hDC,pix_index,&pixelDesc))
  {
    DWORD code = GetLastError();
    return FALSE;
  }

  return TRUE;
}

BOOL GLView3D::CreateImageGLContext(HDC hDC)
{
  m_hGLContext = wglCreateContext(hDC);

  if(m_hGLContext==NULL)
    return FALSE;

  if(wglMakeCurrent(hDC,m_hGLContext)==FALSE)
    return FALSE;

  return TRUE;
}

void GLView3D::RenderImage(void)
{
  if (m_IsInitialized && m_dib.IsCreated())
  {
    glClearColor((float)1.00,(float)1.00,
      (float)1.00,1.0f);

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // put a working copy on the matrix stack
    glPushMatrix();

    // Position / translation / scale
    glTranslated(m_model_matrix.X(),
                 m_model_matrix.Y(),
                 m_model_matrix.Z());
    glRotated(m_model_matrix.XRot(), 1.0, 0.0, 0.0);
    glRotated(m_model_matrix.YRot(), 0.0, 1.0, 0.0);
    glRotated(m_model_matrix.ZRot(), 0.0, 0.0, 1.0);
    glScaled(m_model_matrix.XScale(),
             m_model_matrix.YScale(),
             m_model_matrix.ZScale());

    // build the scene...if necessary
    RenderPrimaryImage();

    glFlush();

    // pop the working copy of the matrix stack
    glPopMatrix();
  }
}

void GLView3D::RenderPrimaryImage(void)
{
  glColor3ub(128,128,128);
  GLUquadricObj * q = gluNewQuadric();
  gluQuadricDrawStyle(q, GLU_LINE);
  gluQuadricNormals(q, GLU_SMOOTH);
  gluSphere(q,1.0,16,16);
  gluDeleteQuadric(q);

  GLdouble v1[3], v2[3];

  glBegin(GL_LINES);
  v1[0] = v1[1] = v1[2] = 0.0;
  v2[0] = 1.0; v2[1] = v2[2] = 0.0;
  glColor3ub(224,0,0);
  glVertex3dv(v1);
  glVertex3dv(v2);
  v2[0] = 0.0; v2[1] = 1.0; v2[2] = 0.0;
  glColor3ub(0,224,0);
  glVertex3dv(v1);
  glVertex3dv(v2);
  v2[0] = 0.0; v2[1] = 0.0; v2[2] = 1.0;
  glColor3ub(0,0,224);
  glVertex3dv(v1);
  glVertex3dv(v2);
  glEnd();
}

void GLView3D::DrawImage(CWnd * wnd)
{
  CClientDC dc(wnd);
  m_dib.Draw(&dc,0,0);
}

Some Information about the Sample Project

This article is NOT intended to be the final word on OpenGL programming. You could probably spend a career on OpenGL and 3D programming and still not know everything.

The sample project has printing code within the view class's "OnDraw" method.

Enjoy.

Downloads

Download demo project - 179 Kb



About the Author

Andy McGovern

Andy McGovern - Software Developer/Engineer. Special interests: astronomy, image processing, orbital mechanics, mathematics. Currently work at the Johns Hopkins University Applied Physics Laboratory on the science operations team for the CRISM instrument on the Mars Reconnaissance Orbiter.