Contents
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 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.
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()
5. NbValues = glRenderMode(GL_RENDER)
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:
- launch the Bin/Mesh application;
- drag the venus.wrl file on it;
- change the viewpoint using right/left/twice mouse buttons;
- select the menu Export/Eps;
- enter a file name;
- launch GhostView and check the result;
- 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);
- 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:
- launch the Bin/Mesh application;
- drag the venus.wrl file on it;
- change the viewpoint using right/left/twice mouse buttons;
- apply three iterations of uniform Loop subdivision using the menu Mesh/Loop
subdivision (or press the key Ctrl+L);
- select a white background (menu OpenGL/clear color);
- select a black mesh color (menu Mesh/Color/Choose);
- check the line rendering mode (menu OpenGL/line);
- uncheck the light option (the small sun on the toolbar);
- choose your culling option preference (menu OpenGL/culling);
- select the menu Export/Eps;
- enter a file name;
- launch GhostView and check the result;
- insert your EPS file in your LateX document;
- check your document.
Here is the EPS export function defined in the view-derived class:
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:
- launch the Bin/Mesh application;
- drag the geosphere.wrl file on it;
- change the viewpoint using right/left/twice mouse buttons;
- select the menu Export/Wmf;
- check the face radio button;
- make a copy;
- open PowerPoint;
- make a new blank document;
- press Ctrl+v (paste);
- come back to the Mesh application;
- check the menu OpenGL/Line;
- press Ctrl+L twice (Loop subdivision);
- replay the sequence from the step 4.
Let us now describe the entire process sequence:
- generate Windows enhanced metafile;
- get its device context, the *painting engine* (called
pMetaDC
);
- get the current OpenGL projection parameters;
- do the by-the-hand projection of the model primitives;
- call GDI drawing functions on
pMetaDC
;
- close the metafile stream;
- push the corresponding buffer to the clipboard.
Following code is called from the "Copy" button from the dialog window
launched by menu Export/wmf...
BeginWaitCursor();
UpdateData(TRUE);
CDC *pDC = m_pDoc->GetView()->GetDC();
ASSERT(pDC);
CRect rect;
m_pDoc->GetView()->GetClientRect(&rect);
rect.InflateRect(5,5);
HDC hMetaDC = CreateEnhMetaFile(pDC->m_hDC,"metafile.emf",NULL,NULL);
if(!hMetaDC)
{
AfxMessageBox("Unable to create MetaFile");
ReleaseDC(pDC);
return;
}
CDC *pMetaDC = CDC::FromHandle(hMetaDC);
ASSERT(pMetaDC);
pMetaDC->SetMapMode(MM_TEXT);
double ratio = 1;
glPushMatrix();
CMeshView *pView = (CMeshView *)m_pDoc->GetView();
glTranslated(pView->m_xTranslation,pView->m_yTranslation,
pView->m_zTranslation);
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);
glScalef(pView->m_xScaling,pView->m_yScaling,pView->m_zScaling);
GLdouble modelMatrix[16];
GLdouble projMatrix[16];
GLint viewport[4];
glGetDoublev(GL_MODELVIEW_MATRIX,modelMatrix);
glGetDoublev(GL_PROJECTION_MATRIX,projMatrix);
glGetIntegerv(GL_VIEWPORT,viewport);
CSceneGraph3d *pScene = &m_pDoc->m_SceneGraph;
for(int i=0;iNbObject();i++)
{
CObject3d *pObject = pScene->GetAt(i);
if(pObject->GetType() == TYPE_MESH3D)
if(m_Mode == MODE_LINE)
((CMesh3d *)pObject)->glDrawProjectLine(pMetaDC,
modelMatrix,
projMatrix,
viewport,
m_ColorLine,
m_Ratio,
rect.Height());
else
((CMesh3d *)pObject)->glDrawProjectFace(pMetaDC,
modelMatrix,
projMatrix,
viewport,
m_ColorLine,
m_ColorFace,
m_Ratio,
rect.Height(),
m_RatioNbFaces);
}
glPopMatrix();
HENHMETAFILE hMetaFile = CloseEnhMetaFile(hMetaDC);
OpenClipboard();
EmptyClipboard();
SetClipboardData(CF_ENHMETAFILE,CopyEnhMetaFile(hMetaFile,NULL));
CloseClipboard();
DeleteEnhMetaFile(hMetaFile);
ReleaseDC(pDC);
EndWaitCursor();
}
Following code corresponds to the glDrawProjectFace
function
from the 3D mesh :
Fig. 6. Mesh editing using PowerPoint after EMF/WMF export, then
conversion to EPS using the shareware wmf2eps from Wolfgang Schulter [2].
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.
void
CMeshView::OnEditCopy()
{
if(OpenClipboard())
{
BeginWaitCursor();
CSize size;
unsigned char *pixel = SnapClient(&size);
CTexture image;
VERIFY(image.ReadBuffer(pixel,size.cx,size.cy,24));
delete [] pixel;
EmptyClipboard();
SetClipboardData(CF_DIB,image.ExportHandle());
CloseClipboard();
EndWaitCursor();
}
}
unsigned char *CMeshView::SnapClient(CSize *pSize)
{
BeginWaitCursor();
CRect rect;
GetClientRect(&rect);
CSize size(rect.Width(),rect.Height());
*pSize = size;
ASSERT(size.cx > 0);
ASSERT(size.cy > 0);
unsigned char *pixel = new unsigned char[3*size.cx*size.cy];
ASSERT(pixel != NULL);
TRACE("Start reading client...\n");
TRACE("Client : (%d,%d)\n",size.cx,size.cy);
CRect ClientRect,MainRect;
this->GetWindowRect(&ClientRect);
CWnd *pMain = AfxGetApp()->m_pMainWnd;
CWindowDC dc(pMain);
pMain->GetWindowRect(&MainRect);
int xOffset = ClientRect.left - MainRect.left;
int yOffset = ClientRect.top - MainRect.top;
for(int j=0;j < size.CY; j++)
for(int i=0;i < size.CX; i++)
{
COLORREF color = dc.GetPixel(i+xOffset,j+yOffset);
pixel[3*(size.cx*(size.cy-1-j)+i)] = (BYTE)GetBValue(color);
pixel[3*(size.cx*(size.cy-1-j)+i)+1] = (BYTE)GetGValue(color);
pixel[3*(size.cx*(size.cy-1-j)+i)+2] = (BYTE)GetRValue(color);
}
EndWaitCursor();
return pixel;
}
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) http://www.wmf2eps.de.vu/
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.
http://reality.sgi.com/opengl/tips/Feedback.html
Date Posted: [10/30/2000]