The Old and New Testament of the graphics hardware pipeline

 

 

This article was originally written for NT Developer (for whom I used to frequently contribute). Alas they are now defunct, and since they paid me for the article, I figure somebody might as well get to read it. My next book will have an entire chapter on printing OpenGL graphics, with coverage for NT and Windows 95. This is the second part of the article OpenGL, Workstation Graphics does Windows. 

Click here to download the samples.

 

OpenGL, Metafiles, and Printing
Richard S. Wright Jr.

 Last month, I explained why rumors of OpenGL’s death have been greatly exaggerated. This month, I shall explore some of the more advanced supporting features of OpenGL in the operating system. Features present in Windows NT, but not Windows 95, and certainly not available under Direct 3D or any other toy 3D API’s.

My first experience with computers was in Mr. Decker’s eight grade Algebra I class in 1978. We had an old terminal of some sort that had a huge acoustic coupler that the telephone handset would fit into. We’d place a call to the Board of Education in Louisville, and then yacky-yack the word "ready" would be printed on a huge roll of paper. After an initial program that counted ducks (up to ten!), we had a real Algebra problem; a paramutual betting game. It made for great fun just before Derby Day.

We had a lot of fun on that old terminal, and I still have some of that paper with print outs exclaiming that the horse "mamma’s combat boots" had placed first. From then on, I was hooked on computers, a fever for which I believe there is no cure (although having children can certainly temper it a bit!). I left paper behind and "graduated" to "Trash-80’s", the TI-99 4A, and various Commodore and Atari flavors before getting through high school. It wasn’t long before I really started to miss something important from that eight grade class room. Hardcopy.

I feel pretty confident in saying that with few exceptions any truly useful computer program or application, has the capability to print its results to the printer. At the very least, you can save something in a file, that you can later reformat and dump to a printer. Computers are tools, and these tools often solve problems that are expressed in some form of output. How do you communicate this output to others? Why you invite them all to come and see what your wonderful computer has produced on the monitor…. Not!

Applications that make use of 3D graphics aren’t terribly different from applications that perform extensive number crunching or database searching. With the possible exception of games, 3D graphics are typically used to clarify some relationship, or display the output of a great deal of design effort. Just like any other application that produces tangible results, you will often need to produce a hardcopy of these results for distribution, or archival purposes. Asking the user to press "print-screen", then open Word, select paste, then print… is a pretty good way to ensure poor reviews of your product.

Applications that produce graphics using any API other than GDI under Windows 95, have very few options for printing their display. The usual method is to render to a bitmap (as opposed to the window), and then print the bitmap. A robust approach requires banding, and a lot of memory to get output of reasonable fidelity. An awkward and kludgey approach at best. If your using OpenGL under Windows 95, another approach to consider is to use OpenGL’s feedback mechanism to produce transformed 2D coordinates that you can then feed to GDI. This actually works for very few general purpose applications, unless your only drawing wireframe images.

With Windows NT (starting with 4.0), the situation when using OpenGL is vastly improved. Not only can you render directly into a printer’s device context, but you can also capture OpenGL commands in an enhanced metafile. Furthermore, color images will be automagically converted to greyscale if your destination printer only supports black and white, such as most common laser printers. Even my old 9 pin Epson produces recognizable output. These two capabilities are actually closely related, as only printer drivers that support enhanced metafile spooling will support OpenGL rendering. Fortunately, this is true for most printers.

To check for metafile support, bring up the properties page for your printer. You should see a button labeled "Print Processor", which brings up the dialog shown in Figure 1 below.

 

Figure 1 - the Print Processor Dialog

Some printers, such as my HP Deskjet, will automatically use EMF spooling even when RAW is selected, others such as my older Epson T-750 will require you to bring up this dialog and manually select EMF as the default datatype.

 

Printing with OpenGL

Let’s get right to the code for printing with OpenGL. Figure 2 shows the output of our first sample program "metademo". This program takes a texture map of the world (more of a relief map really), and applies it to a sphere. You can use the arrow keys to rotate the globe horizontally and vertically. The globe rotates rather slowly because we are loading the texture map from disk every time we render. The reason for this will be clear when we get to the metafile recording and playback later. The file menu contains the commands, "Record", "Playback", and "Print". Right now, we’re just going to examine the print option. 

 

Figure 2 - The Metafile Demo Program

When you select "Print", the Windows common dialog for printing is displayed and you can select any attached or networked printer. When this dialog is dismissed, the globe is rendered to the printer. Note, I chose a white background instead of black as this usually looks better when printed.

The code for metademo is quite lengthy, and I won’t go into all the details of the program, or the OpenGL code that draws a texture mapped sphere. For the purposes of this article, I have to assume you already know how to use OpenGL (that would be a series of articles by itself!), and just want the meat of how to render to a printer.

The function called when "print" is selected does several things. First, it displays the Windows common dialog to get a destination printer and create a device context for that printer. Once a destination device context is established, GetDeviceCaps() is called to get the dimensions of the page (in device coordinates). A pixel format is then selected for the printer context in much the same way it is for a window device context. One caveat is that you should not try to set the pixel format until after you call the StartDoc/StartPage sequence that is necessary for all Windows printing. Once you have set the pixel format, you create the OpenGL rendering context and initialize it just as you would the window rendering context. This means enabling texture mapping, establishing culling modes, etc. It’s good practice to actually use the same function for both the window setup and the printer setup. You also need to initialize your projection, which in this case is orthographic. The function you use for this, ChangeSize() in this sample, can take a width and height argument that you can pass either the window size or page size as appropriate.

Once this work has been done, you simply issue your OpenGL rendering commands and they are performed on the printers device context. When finished, be sure and delete the OpenGL rendering context established for the printer, and restore the windows rendering context as the currently active one. Finally don’t forget to call EndPage/EndDoc just as you would for any Windows print job, and be sure to free the printers device context. The complete listing for the function PrintGL is listed below.

///////////////////////////////////////////////////////////////////////////////
void PrintGL(HWND hWnd, HDC hDC, HGLRC hRC)
{
// Pixel format for Printer Device context
static PIXELFORMATDESCRIPTOR pPrintfd = {
sizeof(PIXELFORMATDESCRIPTOR), // Size of this structure
1, // Version of this structure
PFD_SUPPORT_OPENGL | // Support OpenGL calls
PFD_SUPPORT_GDI | // Allow GDI drawing in window too
PFD_DEPTH_DONTCARE, // Don't care about depth buffering
PFD_TYPE_RGBA, // RGBA Color mode
24, // Want 24bit color
0,0,0,0,0,0, // Not used to select mode
0,0, // Not used to select mode
0,0,0,0,0, // Not used to select mode
0, // Size of depth buffer
0, // Not used to select mode
0, // Not used to select mode 0, // Not used to select mode
0, // Not used to select mode
0,0,0 }; // Not used to select mode
PRINTDLG printData; // Printing data
DOCINFO docInfo; // Document Info
int nPixelFormat; // Pixel format requested
int cxPage; // Dimension of printer page (x)
int cyPage; // Dimension of printer page (y)
// Get printer information
memset(&printData,0,sizeof(PRINTDLG));
printData.lStructSize = sizeof(PRINTDLG);
printData.hwndOwner = hWnd;
printData.hDevMode = NULL;
printData.hDevNames = NULL;
printData.Flags = PD_RETURNDC | PD_ALLPAGES;
// Display printer dialog
if(!PrintDlg(&printData))
return;
// Get the dimensions of the page
cxPage = GetDeviceCaps(printData.hDC, HORZRES);
cyPage = GetDeviceCaps(printData.hDC, VERTRES);
// Initialize DocInfo structure
docInfo.cbSize = sizeof(DOCINFO);
docInfo.lpszDocName = "OpenGL Metafile Rendering";
docInfo.lpszOutput = NULL;
 
// Choose a pixel format that best matches that described in pfd
nPixelFormat = ChoosePixelFormat(printData.hDC, &pPrintfd);
// Watch for no pixel format available for this printer
if(nPixelFormat == 0)
{
// Delete the printer context
DeleteDC(printData.hDC);
MessageBox(hWnd,"Cannot choose a pixel format for this printer!",NULL,
MB_OK | MB_ICONSTOP);
return;
}
// Start the document and page
StartDoc(printData.hDC, &docInfo);
StartPage(printData.hDC);
// Set the pixel format for the device context, but watch for failure
if(!SetPixelFormat(printData.hDC, nPixelFormat, &pPrintfd))
{
// Delete the printer context
DeleteDC(printData.hDC);
MessageBox(hWnd,"Error Setting pixel format for this printer!",NULL,
MB_OK | MB_ICONSTOP);
return;
}
// Create the Rendering context and make it current
HGLRC hPRC = wglCreateContext(printData.hDC);
wglMakeCurrent(printData.hDC, hPRC);
// Setup rendering context state
SetupRC();
// Set viewing volume info
//glViewport(0,0,cxPage/2,cyPage/2); // Put this in to restrict area of page.
ChangeSize(cxPage,cyPage);
// Perform the OpenGL Commands
Render();
glFinish();
// Cleanup the rendering context for the printer
wglMakeCurrent(NULL,NULL);
wglDeleteContext(hPRC);
// Signify page and document done
EndPage(printData.hDC); // Finish writing to the page
EndDoc(printData.hDC); // End the print job
// Delete the printer context when done with it
DeleteDC(printData.hDC);
// Restore window rendering context
wglMakeCurrent(hDC, hRC);
}

 

Take special note that the pixelformat for the printer is not identical to that used for the screen. For starters, we don’t use a double buffered rendering context when printing, so there is also no corresponding SwapBuffers() call. We can also dispense with the PFD_DRAW_TO_WINDOW flag, although leaving this flag in place will not cause an error. You’ll notice a few functions defined elsewhere in the program, SetupRC(), ChangeSize(), and Render(). These functions perform OpenGL setup, scaling, and rendering and are used identically for rendering on screen.

Notice the line

//glViewport(0,0,cxPage/2,cyPage/2);

which is commented out. By default the OpenGL viewport will be the entire window or printer page, but you can change this to restrict the area used. If you uncomment this line and rebuild the example program, your printout will be restricted to the lower left quadrant of the page. This can be very useful when you need to mix OpenGL with GDI rendering on a page and need to place the OpenGL image precisely among the other items.

 

OpenGL and Metafiles

In addition to sending OpenGL commands to a printer, you can also send OpenGL commands to an enhanced metafile, and save them to disk. You can then play back the metafile later into a window that has an OpenGL rendering context. Metafiles that contain OpenGL calls can be used as a sort of persistent display list, or something as simple as OpenGL based clipart.

The metademo program contains a menu option called "record". When this is selected, an enhanced metafile with the name record.emf is created. Selecting "playback" from the menu then loads this metafile and plays it into the window’s OpenGL context, thus recreating the image. To see this in action, start the metademo program and select "record". Then use the arrow keys to move the globe around (perhaps with North America facing you instead of Africa). Then when you select "Playback", you should see the portion of the globe that was captured when you recorded the metafile.

The code that creates the metafile is contained in the function RecordGL(). A pixel format and rendering context is created for the metafile, and all the OpenGL commands executed to create the globe displayed is played into the metafile. Finally, the metafile is closed and the windows rendering context is restored. The entire function is listed below.

 

///////////////////////////////////////////////////////////////////////////////
// Renders OpenGL calls into an enhanced metafile
void RecordGL(HWND hWnd, HDC hDC, HGLRC hRC)
{
HDC hMetaFile; // Metafile handle
RECT rect; // Screen dimensions
PIXELFORMATDESCRIPTOR pfd; // Pixel format descriptor
HGLRC hRC2; // OpenGL Rendering context for metafile
// Open the Metafile and start recording
GetClientRect(hWnd,&rect);
hMetaFile = CreateEnhMetaFile(hDC,"record.emf",&rect,NULL);
// Get the current pixel format description.
int nPF = GetPixelFormat(hDC);
DescribePixelFormat(hDC,nPF,sizeof(PIXELFORMATDESCRIPTOR),&pfd);
// Choose the closest pixel format available for the metafile
nPF = ChoosePixelFormat(hMetaFile,&pfd);
SetPixelFormat(hMetaFile,nPF,&pfd);
// Create the OpenGL context for the metafile
hRC2 = wglCreateContext(hMetaFile);
wglMakeCurrent(hMetaFile,hRC2);
// Setup the new GL context to match the current window GL context
SetupRC();
// Capture the projection being used
ChangeSize(rect.right,rect.bottom);
// Caputre the rendering calls
Render();
// Deselect and destroy the metafiles rendering context
wglMakeCurrent(hMetaFile,NULL);
wglDeleteContext(hRC2);
// Make the Window rendering context current again
wglMakeCurrent(hDC,hRC);
// Close the enhanced metafile
CloseEnhMetaFile(hMetaFile);
DeleteEnhMetaFile(hMetaFile);
}

This function is a bit more generalized than the printing function. To select a pixelformat, we first get a description of the window’s pixelformat, then use ChoosePixelFormat() to allow Windows to select a pixelformat for the metafile that most closely matches the pixelformat used for the window. We don’t need to worry about Windows selecting a double buffered pixelformat for a metafile. Just as in printing, we call the same OpenGL functions to setup our rendering context, setup our projection (this time with window size instead of page size), and finally call our rendering code. The final step is to delete the OpenGL rendering context we created for the metafile, and restore the window’s rendering context. Naturally, we must also close the metafile and delete its handle.

The actual OpenGL code the creates the globe is in Render(). This function is fairly short, and is listed below:

/////////////////////////////////////////////////////////
// Render the OpenGL Scene
void Render()
{
// Clear the window with current clearing color
glClear(GL_COLOR_BUFFER_BIT);
// Save current matrix state
glPushMatrix();
// Rotate model's axis according to key strokes
glRotatef(xRot, 1.0f, 0.0f, 0.0f);
glRotatef(zRot, 0.0f, 0.0f, 1.0f);
// Load the desired texture, and set texture parameters
ooglTexture2D ooglPic1;
ooglPic1.LoadBMP("earthmap.bmp");
ooglPic1.TexParameter(GL_TEXTURE_WRAP_S, GL_REPEAT);
ooglPic1.TexParameter(GL_TEXTURE_WRAP_T, GL_REPEAT);
ooglPic1.TexParameter(GL_TEXTURE_MAG_FILTER, GL_LINEAR);
ooglPic1.TexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 
 
// Draw the texture mapped sphere, it doesn't get any
/// easier than this.
pObj = gluNewQuadric(); // Create Quadric object
gluQuadricTexture(pObj, GL_TRUE);// Automatically generate texture coordinates
gluSphere(pObj, 80.0f, 20, 20); // Draw a sphere (radius 80, 20 slices, 20 stacks)
gluDeleteQuadric(pObj); // Done with quadric
// Restore Matrix State
glPopMatrix();
// Finish drawing commands before continuing
glFinish();
}

 

We didn’t go into the details of this function for printing because for printing the output is captured immediately. When rendering to a metafile, you are storing OpenGL commands and state information for playback later, and there are a few important issues to consider. Most importantly for this sample is the texture state. When rendering to a printer, you are capturing the actual rasterization of the image to hardcopy, which is influenced by whatever values any state variables may contain at the time. When rendering to a metafile it is important to play into the metafile any OpenGL commands that create these initial state conditions. With texturing for example, you will need to capture the actual loading of the texture, not just the use of the texture (which you can get away with when printing). This is why our rendering code here goes through the trouble of actually loading the texture from disk each time it is called. You’d get up to 10 times the speed by loading the texture only once, but then the code would only have worked for the printing example.

The ooglTexture2D class by the way is an very useful C++ class I came up with for quickly loading textures from .bmp files. If you’ve had to write this code yourself in the past, you’ll probably appreciate this little freebie. How to do texturing is also out of the scope of this article, so we’ll leave that to another episode as well.

The code to playback the metafile is virtually trivial. The function PlaybackGL() is listed below.

///////////////////////////////////////////////////////////////////////////////
// Playback a metafile containing OpenGL Calls into the current window
void PlaybackGL(HWND hWnd, HDC hDC)
{
RECT rect; // Window rectangle
HENHMETAFILE hMetaFile; // Metafile handle
// Open the metafile
hMetaFile = GetEnhMetaFile("record.emf");
// Get dimensions ofthe current window
GetClientRect(hWnd,&rect);
// Play the metafile into the OpenGL context
PlayEnhMetaFile(hDC,hMetaFile,&rect);
// Swap buffers
SwapBuffers(hDC);
// Close the metafile
DeleteEnhMetaFile(hMetaFile);
}

For playback, we don’t have to create a rendering context at all, since we already have one active (the window’s). All we have to do is open the metafile, and play it into the window’s device context. We will need to do a buffer swap since the window’s OpenGL pixelformat is double buffered, but that is all. The size of the window, stored in rect, is required. On playback metafiles are automatically scaled to the size of the window they are being played into.

 

OpenGL "Clipart"

If your at all like me, you’ve jumped ahead and already tried to load the record.emf file into Word, or some other program that supports metafiles as a form of clipart, or native data. You’ve also probably been disappointed to find that your "metafiles" seemed to be blank. Unfortunately few Windows applications that support metafiles, also support metafiles that contain OpenGL (you also have to consider that this support is only in NT 4.0 or later, and isn’t even planned for Windows 98). Essentially, a metafile is nothing more than a list of GDI (and now OpenGL) commands that can be "played" into a device context. If that device context does not have a valid pixelformat and OpenGL rendering context selected and created, then any OpenGL commands will be ignored. Thus, unless an application specifically checks the metafile for OpenGL and then accommodates by creating a rendering context, OpenGL will not be supported by the application, even though it is supported by the OS.

Since enhanced metafiles are supported natively by the clipboard, this would be an ideal way to cut and paste OpenGL images from one application to another. Until more applications take OpenGL into consideration, this isn’t likely to be pervasive anytime soon. To make it easier to include OpenGL aware metafile support, I’ve included an OCX called metagl that you can embed in your applications. The metagl OCX will load any enhanced metafile, including those that contain OpenGL.

The metagl OCX was created with Visual C++’s Appwizard, and I will spare you the details of creating a skeleton OCX. The most important method of the OCX, LoadMetaFile() is listed below. The function takes the filename of a metafile to load as its only parameter. First is checks the value of a member variable m_hMetafile to see if a metafile is already loaded. If not, it loads the metafile and checks to see if the metafile has a pixelformat. If it does, then it contains OpenGL calls. The metafiles Pixelformat is used to select a close matching one for the window, and an OpenGL rendering context is created. Finally, the window of the OCX is invalidated to force a repaint, which will now be based on the loaded metafile.

BOOL CMetaGLCtrl::LoadMetaFile(LPCTSTR szFileName)
{
m_csFileName = szFileName;
// If a metafile is loaded, purge it
if(m_hMetaFile)
DeleteEnhMetaFile(m_hMetaFile);
// Attempt to open the enhanced metafile. On Failure m_hMetaFile
// is NULL.
m_hMetaFile = GetEnhMetaFile(m_csFileName);
// Get the pixel format out of the metafile and set it
if(m_hMetaFile != NULL)
{
PIXELFORMATDESCRIPTOR pf;
int nSize = GetEnhMetaFilePixelFormat(m_hMetaFile,sizeof(PIXELFORMATDESCRIPTOR),&pf);
int npf = ChoosePixelFormat(m_hDC,&pf);
SetPixelFormat(m_hDC,npf,&pf);
m_hRC = wglCreateContext(m_hDC);
wglMakeCurrent(m_hDC, m_hRC);
}
InvalidateRect(NULL,TRUE);
if(m_hMetaFile == NULL)
return FALSE;
return TRUE;
}

The OnDraw() function, listed below, just simply plays the enhanced metafile if it is loaded. If the metafile does not contain any OpenGL calls, it is still played, but the call to SwapBuffers() is ignored if there is no OpenGL rendering context.

 

void CMetaGLCtrl::OnDraw(
CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
// If a metafile is loaded, play it
if(m_hMetaFile)
{
PlayEnhMetaFile(m_hDC,m_hMetaFile,&m_rect);
SwapBuffers(m_hDC);
}
}

 

Final Demo

You can load the metagl.ocx into the control test container the comes with Visual C++, and manually call the LoadMetaFile method to see the output. Frankly that’s about as much fun as debugging someone else’s code. To make things easy on you (and myself when testing this stuff), I put together a simple dialog based program that contains the metagl OCX, a Load button, and a text field that displays the metafile filename and path. The Load button displays the file selection common dialog and you can select any enhanced metafile (.emf file) for loading. The message handler is short and sweet.

///////////////////////////////////////////////////////////////////////////////
// This is the button messeage handler, it creates a file dialog and allows
// you to select an enhanced metafile. This metafile is displayed by the
// metagl control. This will display any enhanced metafile, including those
// that contain opengl calls.
void CMetaViewDlg::OnBtnLoad()
{
static char BASED_CODE szFilter[] = "Enhanced Metafiles (*.emf)|*.emf||";
// Create the file dialog
CFileDialog fileDialog(TRUE,"emf",NULL,OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,szFilter);
// Present the file dialog
if(fileDialog.DoModal() != IDCANCEL)
{
// If a file selected, tell the control the name of the file to display
m_MetaViewer.LoadMetaFile(fileDialog.GetPathName());
((CWnd *)GetDlgItem(IDC_METANAME))->SetWindowText(fileDialog.GetPathName());
}
}

 

Figure 3 shows the MetaView program in action. In this instance, I selected the record.emf file that was created by MetaDemo. You can also load any enhanced metafile, with or without OpenGL.

Figure 3 - MetaView program output.

 

Another problem I came up against was a lack of pre-existing enhanced metafiles on my machine to test this with. Microsoft Office ships with hundreds of windows metafiles (.wmf extension) for use as clipart, but these are not the same as enhanced metafiles. One valuable resource I found on the Internet was a handy little utility called "Metafile Companion", which can be found at: http://www.companionsoftware.com/Products/MetafileCompanion/index.html.

This little dodad will load any windows metafile (.wmf), and will convert it to an enhanced metafile (.emf). I turned lot’s of the clip art that comes with Office into enhanced metafiles with ease. It will also load enhanced metafiles for viewing or conversion, but unfortunately doesn’t yet support metafiles with OpenGL.

Conclusion

OpenGL is well supported by Microsoft for delivering technical applications on the Windows NT platform. Although OpenGL is portable, any application ported to Windows must take into consideration the many Windows specific idiosyncrasies. Printing can be one of the most daunting aspects of porting code to Windows, as well as one of the most complex portions of a Windows application. With native support for OpenGL printing, Microsoft has made it very easy to write full featured and robust OpenGL based applications for Windows NT. Support for OpenGL in metafiles lays the ground work for 3D graphics programs that are fully integrated with other operating system features such as clipboard and OLE support. As Windows 95/98 and NT 5.0 gradually evolve to a single operating system based on NT, we can only expect that these features will become more pervasive in the future.