Click here to Skip to main content
Click here to Skip to main content

Achieving PostScript and Wmf outputs for OpenGL

By , 9 Jun 2003
Rate this:
Please Sign up or sign in to vote.



This article explains how to generate resolution independent versions of 3D meshes rendered by OpenGL/MFC programs, i.e. how to export the rendering results to vector formats such as encapsulated postscript (EPS) and Windows enhanced metafile (EMF) formats. The main goal consists of being able to generate vector figures for editing, printing and illustrating purposes. We assume that the mesh models are stored in vrml 97 files exported via 3D Studio max.

After having read this article and downloaded the full project, you would be able to:

  • display a vrml 3D triangular mesh using OpenGL in a MFC MDI application,
  • change the rendering options such as wire-frame, smooth shading, lighting and culling,
  • export the rendering result of the current mesh to encapsulated postscript (EPS) format,
  • export to Windows enhanced metafile (EMF) format through the clipboard,
  • copy the rendered image to the clipboard using the DIB format,
  • apply mesh subdivision using the uniform Loop subdivision scheme [1].
Although this article encloses a small 3D library designed for surface subdivision, it especially focuses on the export features. In particular, the VRML parser is a very simple one implemented for an illustration purpose and is not supported. Figure 1 summarizes the export functions offered by this article.

Fig 1. From left to right: the proposed MFC MDI/OpenGL application displaying a triangular mesh, the postscript viewer ghostview after having exported the model to encapsulated postscript (EPS) format, PowerPoint displaying the model after having exported the model to windows enhanced metafile (EMF) format via the clipboard and Paint shop pro having received the clipboard device independent bitmap (DIB) content.


1200 dots per inch. This is the graphic resolution claimed by your last high end laser postscript printer. It would be great to render your highly detailed 3D meshes using such a fine resolution. For instance, the printing result of the rendered image, even at the highest printer resolution, leads to huge amount of memory space, blocky effects, and even aliasing. A fairly good solution would be to output a mesh in a resolution independent format such as EPS (Encapsulated PostScript) or EMF (windows Enhanced MetaFile) in order to edit it, then print it using the true printer resolution.

Thanks to the powerful feedback mechanism provided by OpenGL, the postscript output problem can be issued. This article is strongly inspired from Mark J. Kilgard and Frederic Delhoume works, and derives it in order to make it run in a MFC/MDI application. Here is the article I started from.

The EMF format is issued using the gluProject command, a z-sorting of the triangle items, and GDI 2D drawing functions. The article also describes how to push the corresponding EMF stream to the clipboard in order to allow us to make a "paste" in your favorite drawing tool.

A low but robust DIB format output is also presented in order to propose a fix to a few bugs encountered with the glReadPixels function for some graphic cards (see previous article).

For an illustration purpose of high resolution printing, the Loop subdivision scheme is proposed. This function allows us to generate highly detailed meshes from coarse ones provided as VRML sample files by the demo project.

Let us now have a look to the presented application.

The Application

The application is based on the MDI MFC architecture and the graphic library OpenGL. It offers the minimal set of features for opening, displaying and converting 3D triangular meshes to EPS, EMF and DIB formats. Figure 2 describes the application toolbar, Figure 3 illustrates several rendering modes on the nefertiti mesh and Figure 4 shows an additional snapshot of the application having opened four meshes.

Fig 2. The application toolbar. From left to right, and for each annotated button group respectively: the copy and exportation features allow us to i) snap the current OpenGL client window in the clipboard under the DIB format, ii) generate a WMF stream in the clipboard composed of 2D line or/and triangle primitives calculated in the image space according to the current projection matrix, each triangle being z-sorted, and iii) generate an EPS file from the current OpenGL rendering process. The rendering modes correspond to vertex, line and triangle filling, while the main options are smooth (Gouraud) shading, edge superimposing and a light toggle. The culling option may also be switched through the OpenGL menu. The color button group allows ones to change the mesh face and vertex colors, to apply a rainbow color ramp according to y-coordinate (this menu has been added for the WMF illustration purpose), and to change the OpenGL clear color (i.e. the background color).

Fig. 3. Several rendering modes are illustrated with mesh nefertiti.wrl. Top line: vertex, line and face mode. Bottom line: face mode with smooth shading, the same with superimposed edges and the mesh colored according to y-coordinate of each vertex and a rainbow ramp. Every modes but the superimposed can be exported to encapsulated postscript format.

Fig 4. Application snapshot. Four meshes are opened and rendered with different options detailed by Fig. 3. The NMT (numerical model terrain) has been colored according to the elevation.

Export To Encapsulated Postscript

The goal is to generate an EPS file from the 3D mesh seen under the current viewpoint used by OpenGL. We thus extract 2D primitives from 3D triangles thanks to the GL_FEEDBACK rendering mode provided by OpenGL, before calling postscript 2D drawing functions. Corresponding geometric primitives may be circles, strokes, and fillings depending on the chosen OpenGL rendering mode (vertex, line or face respectively). The GL_FEEDBACK rendering mode has been implemented by SGI for the sake of debugging, and outputs the 2D geometric primitives resulting from the rendering process in a float buffer (called pFeedbackBuffer below). From this buffer, the postscript rendering engine (called CPsRenderer below) extracts the geometric primitives and outputs corresponding drawing functions in a EPS file (click here to download sample EPS files). The C++ class CPsRenderer encapsulates the C code written by Mark J. Kilgard and Frederic Delhoume [3].

The following pseudo-code summarizes the sequence:

1. pFeedbackBuffer = new float [size] 
2. glFeedbackBuffer(size,GL_3D_COLOR,pFeedbackBuffer) 

3. glRenderMode(GL_FEEDBACK) 
4. scene.Render() // immediate mode 
5. NbValues = glRenderMode(GL_RENDER) // go back to rendering mode 
6. PsRenderer.Run(pFilename,pFeedbackBuffer,NbValues,TRUE) 
7. delete[] pFeedbackBuffer 

Notice that this code runs fairly good for single color primitives (dots, triangles and line segments), i.e. when the smooth shading/coloring is cut off. The smooth rendering effects are rather issued by recursive subdivision of triangle and line segment primitives until the color difference is smaller than predefined thresholds defined in the file PsRender.h. Therefore take care that the EPS file size may strongly depend on these thresholds.

Want to try an example of exporting to EPS format ? Apply the following sequence:

  1. launch the Bin/Mesh application;
  2. drag the venus.wrl file on it;
  3. change the viewpoint using right/left/twice mouse buttons;
  4. select the menu Export/Eps;
  5. enter a file name;
  6. launch GhostView and check the result;
  7. change the rendering options using the OpenGL menu (notice that the edge superimposing option is not exported, and that a four bits depth graphic display set in GhostView options may lead to some artifacts on the edges. Do not worry about this as it will be removed during printing);
  8. replay the sequence from the step 3.

For a black and white rendering of a very dense mesh that would require your famous 1200 dpi postscript laser printer, apply the following sequence:

  1. launch the Bin/Mesh application;
  2. drag the venus.wrl file on it;
  4. apply three iterations of uniform Loop subdivision using the menu Mesh/Loop subdivision (or press the key Ctrl+L);
  5. select a white background (menu OpenGL/clear color);
  6. select a black mesh color (menu Mesh/Color/Choose);
  7. check the line rendering mode (menu OpenGL/line);
  8. uncheck the light option (the small sun on the toolbar);
  9. choose your culling option preference (menu OpenGL/culling);
  10. select the menu Export/Eps;
  11. enter a file name;
  12. launch GhostView and check the result;
  13. insert your EPS file in your LateX document;
  14. check your document.

Here is the EPS export function defined in the view-derived class:

/ OnExportEps 
void CMeshView::OnExportEps() 
 static char BASED_CODE filter[] = "EPS Files (*.eps)|*.eps"; 
 CFileDialog SaveDlg(FALSE,"*.eps","mesh.eps",
                     OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,filter); 
 if(SaveDlg.DoModal() == IDOK) 
  CString string = SaveDlg.GetPathName(); 
  char *pFilename = string.GetBuffer(MAX_PATH); 

  // Allocation 
  // no way to predict this, you may change it 
  // for large meshes. 
  const int size = (int)6e6; 
  GLfloat *pFeedbackBuffer = new GLfloat[size]; 

  CDC *pDC = GetDC(); 

  // Useful in multidoc templates 

  // Set feedback mode 

  // Render 

  // Position / translation / scale 

   ::glRotatef(m_xRotation, 1.0, 0.0, 0.0); 
   ::glRotatef(m_yRotation, 0.0, 1.0, 0.0); 
   ::glRotatef(m_zRotation, 0.0, 0.0, 1.0); 

   // Start rendering 
   CMeshDoc *pDoc = GetDocument(); 

   // Std rendering (no superimposed lines anyway) 
   // do not use display lists here ! 


  // Double buffer 

  int NbValues = glRenderMode(GL_RENDER); 

  // The export stuff here 
  // This object encapsulates the code from Mark Kilgard, 
  // and adapted by Frederic Delourme 
  CPsRenderer PsRenderer; 

  // Cleanup 
  delete [] pFeedbackBuffer; 

Export To Windows Enhanced Metafile Clipboard

Our goal is to generate a WMF stream in the clipboard from the 3D mesh seen under the current viewpoint used by OpenGL. We thus extract 2D primitives from 3D triangles through a by-the-hand projection using the OpenGL gluProject command. We then call standard GDI 2D drawing functions such as CDC::MoveTo, CDC::LineTo and CDC::Polygon depending on the chosen WMF rendering mode (line or face). For the simple line mode (without culling), a direct projection is sufficient. For the face mode, one has to simulate a z-buffer, which is more difficult (the famous painter's problem). More simply, we compute each face barycenter as z-reference and sort the triangles by this primitive average depth (Fig. 5). This does not disambiguate some cases, and handling these cases would require breaking up the primitives (hope this issue will be addressed in a future article). The most obvious benefit of the WMF formats lies in the fact that it allows ones to edit the mesh in a drawing tool (such as PowerPoint), to add captions, symbols, arrows and every added information that often makes a figure more understandable. Notice that you can also come back to an EPS file after editing in PowerPoint thanks to the great shareware wmf2eps [2] (see Fig. 6).

Fig. 5. The mesh knot.wrl is rendered in PowerPoint using edge and triangle primitives, these ones being sorted according to z-coordinates of each face barycenter. This example illustrates the progressive rendering of the mesh such as one can see it in PowerPoint. Such a sorting by average depth does not disambiguate some cases on silhouette areas or intersecting polygons. Addressing this issue would require to break up the triangle primitives, while the more simple used test is good enough for lots of examples. Click here to download a PowerPoint example file with the venus and the knot meshes.

Want to try an example of mesh editing in PowerPoint ? Apply the following sequence:

  1. launch the Bin/Mesh application;
  2. drag the geosphere.wrl file on it;
  4. select the menu Export/Wmf;
  5. check the face radio button;
  6. make a copy;
  7. open PowerPoint;
  8. make a new blank document;
  9. press Ctrl+v (paste);
  10. come back to the Mesh application;
  11. check the menu OpenGL/Line;
  12. press Ctrl+L twice (Loop subdivision);
  13. replay the sequence from the step 4.

Let us now describe the entire process sequence:

  1. generate Windows enhanced metafile;
  2. get its device context, the *painting engine* (called pMetaDC);
  3. get the current OpenGL projection parameters;
  4. do the by-the-hand projection of the model primitives;
  5. call GDI drawing functions on pMetaDC;
  6. close the metafile stream;
  7. push the corresponding buffer to the clipboard.

Following code is called from the "Copy" button from the dialog window launched by menu Export/wmf...


 // Get DC 
 CDC *pDC = m_pDoc->GetView()->GetDC(); 

 // Get view rect 
 CRect rect; 

 // Create metafile device context 
 // the filename is fixed because 
 // the wmf stream will be pushed in the clipboard 
 // you can easily derive a file-exporter from this. 
 HDC hMetaDC = CreateEnhMetaFile(pDC->m_hDC,"metafile.emf",NULL,NULL); 
  AfxMessageBox("Unable to create MetaFile"); 

 // Get DC from handle 
 CDC *pMetaDC = CDC::FromHandle(hMetaDC); 
 double ratio = 1; 

 // Position / translation / scale from current view 
 CMeshView *pView = (CMeshView *)m_pDoc->GetView(); 
 glRotatef(pView->m_xRotation, 1.0, 0.0, 0.0); 
 glRotatef(pView->m_yRotation, 0.0, 1.0, 0.0); 
 glRotatef(pView->m_zRotation, 0.0, 0.0, 1.0); 

 // Get OpenGL parameters 
 GLdouble modelMatrix[16]; 
 GLdouble projMatrix[16]; 
 GLint viewport[4]; 

 // Start rendering via std GDI 2D drawing functions 
 CSceneGraph3d *pScene = &m_pDoc->m_SceneGraph; 
 for(int i=0;i<PSCENE->NbObject();i++) 
  CObject3d *pObject = pScene->GetAt(i); 
  if(pObject->GetType() == TYPE_MESH3D) // meshes only 
   // The line mode (no sort) 
   if(m_Mode == MODE_LINE) 
    ((CMesh3d *)pObject)->glDrawProjectLine(pMetaDC, 
    // The face mode (faces are z-sorted 
    // according to their barycenter) 
    ((CMesh3d *)pObject)->glDrawProjectFace(pMetaDC, 


 // Close metafile 
 HENHMETAFILE hMetaFile = CloseEnhMetaFile(hMetaDC); 

 // Fill the clipboard (direct sent to wmf2eps or 
 // any windows app such as Powerpoint) 

 // Cleanup 


Following code corresponds to the glDrawProjectFace function from the 3D mesh :

/ glDrawProjectFace 
void CMesh3d::glDrawProjectFace(CDC *pDC, 
                                double *modelMatrix, 
                                double *projMatrix, 
                                int *viewport, 
                               COLORREF ColorLine, 
                               COLORREF ColorFace, 
                                double ratio, 
                                int height, // the window height 
                                float RatioNbFace) // default -> 1.0 
 TRACE("Draw projected mesh in metafile-based device context\n"); 
 TRACE("  face mode\n"); 
 TRACE("  viewport : (%d;%d;%d;%d)\n",viewport[0],viewport[1],
 TRACE("  model : %g\t%g\t%g\n",modelMatrix[0],modelMatrix[1],
 TRACE("          %g\t%g\t%g\n",modelMatrix[3],modelMatrix[4],
 TRACE("          %g\t%g\t%g\n",modelMatrix[6],modelMatrix[7],
 TRACE("   proj : %g\t%g\t%g\n",projMatrix[0],projMatrix[1],projMatrix[2]); 
 TRACE("          %g\t%g\t%g\n",projMatrix[3],projMatrix[4],projMatrix[5]); 
 TRACE("          %g\t%g\t%g\n",projMatrix[6],projMatrix[7],projMatrix[8]); 
 CWmfFace *pArray = new CWmfFace[m_ArrayFace.GetSize()]; 

 // AVL fast z-sorting (from Gaspard Breton) 
 CAVL<CWmfFace,double> avl; 
 CWmfFace bidon; 
 avl.Register(&bidon,&bidon.zc,&bidon.avl); // z as key 

 int NbFaces = m_ArrayFace.GetSize(); 
 TRACE("  %d faces\n",NbFaces); 
 int NbFacesToProcess = (int)(RatioNbFace*(float)NbFaces); 
 TRACE("  %d faces to process\n",NbFacesToProcess); 
 TRACE("  begin sort..."); 
 int NbFaceValid = 0; 
 for(int i=0;i<NbFaces;i++) 
  CFace3d *pFace = m_ArrayFace[i]; 

  // Compute barycenter as z-reference 
  // Sorting by a triangle average depth does not allow 
  // to disambiguate some cases.  Handling these cases would 
  // require breaking up the primitives. Please mail any 
  // improvement about this :-) 
  double xc = (pFace->v1()->x()+pFace->v2()->x()+pFace->v3()->x())/3; 
  double yc = (pFace->v1()->y()+pFace->v2()->y()+pFace->v3()->y())/3; 
  double zc = (pFace->v1()->z()+pFace->v2()->z()+pFace->v3()->z())/3; 

  // Project barycenter 

  // Project three vertices 



  // Crop & sort 
  if(pArray[i].x1 < viewport[0]  || pArray[i].y1 < viewport[1]  || 
     pArray[i].x1 > viewport[2]  || pArray[i].y1 > viewport[3]  || 
     pArray[i].x2 < viewport[0]  || pArray[i].y2 < viewport[1]  || 
     pArray[i].x2 > viewport[2]  || pArray[i].y2 > viewport[3]  || 
     pArray[i].x3 < viewport[0]  || pArray[i].y3 < viewport[1]  || 
     pArray[i].x3 > viewport[2]  || pArray[i].y3 > viewport[3]) 
   pArray[i].m_Draw = 1;  // yes, insert this triangle 
   pArray[i].zc *= -1.0f; // back to front 
   avl.Insert(pArray,i);  // insert via sort 

 // Draw 
 CPen pen(PS_SOLID,0,ColorLine); 
 CBrush BrushFace(ColorFace); 
 CPen *pOldPen = pDC->SelectObject(&pen); 
 POINT points[3]; // triangular faces only 

 // Default 
 CBrush *pOldBrush = pDC->SelectObject(&BrushFace); 
 TRACE("begin draw..."); 
 int nb = 0; 
     (AVLNULL != i) && nb < NbFacesToProcess; 
  // Fill and outline the face 
  points[0].x = (int)(ratio*pArray[i].x1); 
  points[0].y = (int)(ratio*((float)height-pArray[i].y1)); 
  points[1].x = (int)(ratio*pArray[i].x2); 
  points[1].y = (int)(ratio*((float)height-pArray[i].y2)); 
  points[2].x = (int)(ratio*pArray[i].x3); 
  points[2].y = (int)(ratio*((float)height-pArray[i].y3)); 

  // Fill triangle 

  // Outline triangle 

 // Restore and cleanup 
 delete [] pArray; 

Fig. 6. Mesh editing using PowerPoint after EMF/WMF export, then conversion to EPS using the shareware wmf2eps from Wolfgang Schulter [2].

Export To Device Independent Bitmap Clipboard

This section is an alternative to a previous article that explains how to dump the rendered image from an OpenGL program. I had lots of messages about this previous article noticing bugs for various graphic cards and resolutions. It seems that the glReadPixels function is not robust enough to ensure the dump success. We thus use the CDC::GetPixel GDI function in order to push each pixel in a DIB buffer. We then use the intuitive sequence Open/Empty/SetData/Close associated with the Windows clipboard.

// OnEditCopy
 // Clean clipboard content, and copy the DIB. 

   // Snap (see below) 
   CSize size; 
   unsigned char *pixel = SnapClient(&size); 

   // Image 
   CTexture image; 

  // Link image - buffer 

  // Cleanup memory 
  delete [] pixel; 
  SetClipboardData(CF_DIB,image.ExportHandle()); // short is better 

// Hand-made client snapping 
// Slow and heavy.... but more robust than the 
// glReadPixels command (see previous article) 
unsigned char *CMeshView::SnapClient(CSize *pSize) 
 // Client zone 
 CRect rect; 
 CSize size(rect.Width(),rect.Height()); 
 *pSize = size; 
 ASSERT( > 0); 
 ASSERT( > 0); 
 // Alloc 
 unsigned char *pixel = new unsigned char[3**]; 
 ASSERT(pixel != NULL); 

 // Capture frame buffer 
 TRACE("Start reading client...\n"); 
 TRACE("Client : (%d,%d)\n",,; 
 CRect ClientRect,MainRect; 
 CWnd *pMain = AfxGetApp()->m_pMainWnd; 
 CWindowDC dc(pMain); 
 int xOffset = ClientRect.left - MainRect.left; 
 int yOffset = -; 
 for(int j=0;j < size.CY; j++)
 for(int i=0;i < size.CX; i++) 
  COLORREF color = dc.GetPixel(i+xOffset,j+yOffset); // slow but reliable 
  pixel[3*(*(] = (BYTE)GetBValue(color); 
  pixel[3*(*(] = (BYTE)GetGValue(color); 
  pixel[3*(*(] = (BYTE)GetRValue(color); 
 return pixel; 

Mesh Subdividision

Subdivision surfaces define a smooth surface as the limit of a sequence of successive refinement steps applied onto a base mesh. Such techniques offer a number of benefits, including geometry compression, animation, editing, scalability and adaptive rendering. The presented application implements the Loop subdivision scheme, developed by Charles Loop [1]. It combines a 1-to-4 uniform subdivision of each face with a geometric filtering processing that guaranties the smoothness of the limit surface.

Fig. 7. Two iterations of uniform Loop subdivision applied to the mesh venus.wrl. Each successive subdivision iteration makes the surface rather smooth and thus removes its polygonal aspect. The *star* effect visible on the left image, and coming from the the linear Gouraud algorithm is also removed.


Many thanks to Mark J. Kilgard and Frederic Delhoume for implementing the initial postscript exporter. A very special thanks to Gaspard Breton for implementing the fast z-sorting algorithm using AVL trees.


[1] Charles Loop.
Smooth surface subdivision based on triangles.
University of Utah, departement of Mathematics.
Master's thesis. 1987.

[2] Wolfgang Schulter.
Shareware wmf2eps. version 1.2 (02 Apr 2000)
Simply converting WMF into EPS files using a Windows 95/98/NT/2000 PostScript printer driver.

[3] Mark J. Kilgard and Frederic Delhoume.
Achieving Quality Postscript output for OpenGL.


Date Posted: [10/30/2000]



A list of licenses authors might use can be found here

About the Author

Pierre Alliez

France France
No Biography provided

Comments and Discussions

GeneralDirectX Pinmembergeiwo28-Sep-09 8:37 



| Advertise | Privacy | Mobile
Web02 | 2.8.140315.1 | Last Updated 10 Jun 2003
Article Copyright 2001 by Pierre Alliez
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid