为 OpenGL 实现 PostScript 和 Wmf 输出






4.96/5 (41投票s)
2001 年 1 月 17 日

529117

10699
本文解释了如何为 OpenGL/MFC 程序渲染的 3D 网格生成独立于分辨率的版本,即如何将渲染结果导出为矢量格式,如封装的 PostScript (EPS) 和 Windows 增强型图元文件 (EMF) 格式。主要目标是能够
目录
- 摘要
- 引言
- 应用程序
- 导出到封装的 PostScript (EPS)
- 导出到 Windows 增强型图元文件剪贴板 (EMF)
- 导出到设备无关位图 (DIB) 剪贴板
- 网格细分
- 下载次数
- 致谢
- 参考文献
- 历史
摘要
本文解释了如何为 OpenGL/MFC 程序渲染的 3D 网格生成独立于分辨率的版本,即如何将渲染结果导出为矢量格式,如封装的 PostScript (EPS) 和 Windows 增强型图元文件 (EMF) 格式。主要目标是能够生成用于编辑、打印和插图目的的矢量图形。我们假设网格模型存储在通过 3D Studio max 导出的 vrml 97 文件中。阅读本文并下载完整项目后,您将能够
- 在 MFC MDI 应用程序中使用 OpenGL 显示 vrml 3D 三角网格,
- 更改渲染选项,如线框、平滑着色、光照和剔除,
- 将当前网格的渲染结果导出为封装的 PostScript (EPS) 格式,
- 通过剪贴板导出到 Windows 增强型图元文件 (EMF) 格式,
- 使用 DIB 格式将渲染图像复制到剪贴板,
- 使用统一的 Loop 细分方案对网格进行细分 [1]。
图 1。 从左到右:提出的 MFC MDI/OpenGL 应用程序显示三角网格,PostScript 查看器 ghostview 在将模型导出为封装的 PostScript (EPS) 格式后,PowerPoint 在通过剪贴板将模型导出为 Windows 增强型图元文件 (EMF) 格式后显示模型,以及 Paint shop pro 接收到剪贴板的设备无关位图 (DIB) 内容。
引言
1200 dpi。这是您最新的高端激光 PostScript 打印机声称的图形分辨率。能够使用如此精细的分辨率渲染您高度详细的 3D 网格将是很好的。例如,即使在最高打印机分辨率下,渲染图像的打印结果也会导致大量的内存空间、块状效果,甚至锯齿。一个相当好的解决方案是输出一个独立于分辨率的格式的网格,如 EPS (Encapsulated PostScript) 或 EMF (Windows Enhanced MetaFile),以便进行编辑,然后使用真实的打印机分辨率进行打印。得益于 OpenGL 提供的强大的反馈机制,可以解决 PostScript 输出问题。本文强烈受到 Mark J. Kilgard 和 Frederic Delhoume 作品的启发,并将其衍生出来,以便在 MFC/MDI 应用程序中运行。我开始的这篇文章可以在 这里找到。
通过 gluProject
命令、三角形项的 z 排序以及 GDI 2D 绘图函数来生成 EMF 格式。本文还介绍了如何将相应的 EMF 流推送到剪贴板,以便我们可以在您喜欢的绘图工具中进行“粘贴”。
还提供了一种低但稳定的 DIB 格式输出,以解决某些显卡在使用 glReadPixels
函数时遇到的一些错误(请参阅之前的 文章)。
为了说明高分辨率打印的目的,提出了 Loop 细分方案。此函数允许我们从演示项目 [demo project] 提供的 VRML 示例文件中粗略生成的模型生成高度详细的模型。
现在让我们来看看提出的应用程序。
应用程序
该应用程序基于 MDI MFC 体系结构和 OpenGL 图形库。它提供了用于打开、显示和将 3D 三角网格转换为 EPS、EMF 和 DIB 格式的最小功能集。图 2 描述了应用程序工具栏,图 3 说明了 Nefertiti 网格的几种渲染模式,图 4 显示了打开四个网格的应用程序的附加快照。
图 2。 应用程序工具栏。从左到右,以及每个带注释的按钮组分别:复制和导出功能允许我们 i) 在 DIB 格式下将当前 OpenGL 客户端窗口捕捉到剪贴板,ii) 在由图像空间中根据当前投影矩阵计算出的 2D 线或/和三角形图元组成的剪贴板中生成 WMF 流,每个三角形都经过 z 排序,iii) 从当前 OpenGL 渲染过程生成 EPS 文件。渲染模式对应于顶点、线条和三角形填充,而主要选项是平滑(Gouraud)着色、边叠加和灯光切换。剔除选项也可以通过 OpenGL 菜单进行切换。颜色按钮组允许更改网格面和顶点颜色,根据 y 坐标应用彩虹色阶(此菜单已为 WMF 插图目的添加),以及更改 OpenGL 清除颜色(即背景颜色)。
图 3。 使用 mesh nefertiti.wrl 说明了几种渲染模式。顶行:顶点、线条和面模式。底行:带平滑着色的面模式,同样带有叠加的边,以及根据每个顶点的 y 坐标和彩虹色阶着色的网格。除叠加模式外,所有模式都可以导出为封装的 PostScript 格式。
图 4。 应用程序快照。打开了四个网格,并使用图 3 中详细说明的各种选项进行渲染。NMT(数值模型地形)已根据海拔高度进行了着色。
导出到封装的 PostScript
目标是从当前 OpenGL 视点下的 3D 网格生成 EPS 文件。因此,我们在调用 PostScript 2D 绘图函数之前,利用 OpenGL 提供的GL_FEEDBACK
渲染模式从 3D 三角形提取 2D 图元。相应的几何图元可能是圆、笔触和填充,具体取决于选择的 OpenGL 渲染模式(分别为顶点、线条或面)。GL_FEEDBACK
渲染模式由 SGI 为调试目的实现,并将渲染过程产生的 2D 几何图元输出到浮点缓冲区(下文称为 pFeedbackBuffer
)。从该缓冲区中,PostScript 渲染引擎(下文称为 CPsRenderer
)提取几何图元,并将相应的绘图函数输出到 EPS 文件(单击 此处 下载示例 EPS 文件)。C++ 类 CPsRenderer
封装了 Mark J. Kilgard 和 Frederic Delhoume 编写的 C 代码 [3]。以下伪代码总结了该序列
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
请注意,此代码对于单色图元(点、三角形和线段)运行良好,即当平滑着色/颜色被关闭时。平滑渲染效果是通过递归细分三角形和线段图元来产生的,直到颜色差异小于 PsRender.h 文件中定义的预设阈值。因此,请注意 EPS 文件的大小可能在很大程度上取决于这些阈值。
想尝试导出到 EPS 格式的示例吗?执行以下序列
- 启动 Bin/Mesh 应用程序;
- 将 venus.wrl 文件拖到它上面;
- 使用鼠标的左/右/双击按钮更改视点;
- 选择菜单 Export/Eps;
- 输入文件名;
- 启动 GhostView 并检查结果;
- 使用 OpenGL 菜单更改渲染选项(请注意,边缘叠加选项未导出,并且 GhostView 选项中设置的四位深度图形显示可能会导致边缘出现一些伪影。不必担心,因为在打印时会被移除);
- 从步骤 3 重复该序列。
要进行非常密集的网格的黑白渲染,它需要您著名的 1200 dpi PostScript 激光打印机,请执行以下序列
- 启动 Bin/Mesh 应用程序;
- 将 venus.wrl 文件拖到它上面;
- 使用鼠标的左/右/双击按钮更改视点;
- 使用菜单 Mesh/Loop subdivision(或按 Ctrl+L 键)应用三次统一 Loop 细分;
- 选择白色背景(菜单 OpenGL/clear color);
- 选择黑色网格颜色(菜单 Mesh/Color/Choose);
- 检查线条渲染模式(菜单 OpenGL/line);
- 取消选中灯光选项(工具栏上的小太阳);
- 选择您的剔除选项偏好(菜单 OpenGL/culling);
- 选择菜单 Export/Eps;
- 输入文件名;
- 启动 GhostView 并检查结果;
- 将您的 EPS 文件插入到您的 LateX 文档中;
- 检查您的文档。
以下是衍生视图类中定义的 EPS 导出函数
/********************************* / 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]; ASSERT(pFeedbackBuffer); CDC *pDC = GetDC(); // Useful in multidoc templates ::wglMakeCurrent(pDC->m_hDC,m_hGLContext); // Set feedback mode ::glFeedbackBuffer(size,GL_3D_COLOR,pFeedbackBuffer); ::glRenderMode(GL_FEEDBACK); // Render ::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Position / translation / scale ::glPushMatrix(); ::glTranslated(m_xTranslation,m_yTranslation,m_zTranslation); ::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); ::glScalef(m_xScaling,m_yScaling,m_zScaling); // Start rendering CMeshDoc *pDoc = GetDocument(); // Std rendering (no superimposed lines anyway) // do not use display lists here ! pDoc->m_SceneGraph.glDrawDirect(); ::glPopMatrix(); // Double buffer SwapBuffers(pDC->m_hDC); int NbValues = glRenderMode(GL_RENDER); // The export stuff here // This object encapsulates the code from Mark Kilgard, // and adapted by Frederic Delourme CPsRenderer PsRenderer; PsRenderer.Run(pFilename,pFeedbackBuffer,NbValues,TRUE); // Cleanup string.ReleaseBuffer(); delete [] pFeedbackBuffer; ReleaseDC(pDC); } }
导出到 Windows 增强型图元文件剪贴板
我们的目标是从 OpenGL 当前视点下的 3D 网格生成剪贴板中的 WMF 流。因此,我们通过使用 OpenGLgluProject
命令的手动投影来提取 2D 图元。然后,我们根据所选的 WMF 渲染模式(线条或面)调用标准的 GDI 2D 绘图函数,如 CDC::MoveTo
、CDC::LineTo
和 CDC::Polygon
。对于简单的线条模式(无剔除),直接投影就足够了。对于面模式,必须模拟 z 缓冲区,这更难(著名的画家问题)。更简单地说,我们计算每个面的重心作为 z 参考,并按此基本平均深度对三角形进行排序(图 5)。这并没有区分某些情况,处理这些情况需要分解图元(希望这个问题将在以后的文章中解决)。WMF 格式最明显的优势在于它允许在绘图工具(如 PowerPoint)中编辑网格,添加标题、符号、箭头以及通常能使图形更易于理解的附加信息。请注意,通过伟大的共享软件 wmf2eps [2],您也可以在 PowerPoint 中编辑后返回到 EPS 文件(参见 图 6)。图 5。 网格 knot.wrl 在 PowerPoint 中使用边和三角形图元进行渲染,这些图元根据每个面重心的 z 坐标进行排序。此示例说明了网格在 PowerPoint 中的渐进式渲染。这种按平均深度的排序并不能区分轮廓区域或相交多边形的一些情况。解决这个问题需要分解三角形图元,而使用的更简单的测试对于许多示例来说已经足够了。单击 此处 下载包含 venus 和 knot 网格的 PowerPoint 示例文件。
想尝试在 PowerPoint 中编辑网格的示例吗?执行以下序列
- 启动 Bin/Mesh 应用程序;
- 将 geosphere.wrl 文件拖到它上面;
- 使用鼠标的左/右/双击按钮更改视点;
- 选择菜单 Export/Wmf;
- 选中面单选按钮;
- 进行复制;
- 打开 PowerPoint;
- 创建一个新的空白文档;
- 按 Ctrl+v(粘贴);
- 回到 Mesh 应用程序;
- 检查菜单 OpenGL/Line;
- 按两次 Ctrl+L(Loop 细分);
- 从步骤 4 重复该序列。
现在让我们描述整个过程序列
- 生成 Windows 增强型图元文件;
- 获取其设备上下文,“绘图引擎”(称为
pMetaDC
); - 获取当前的 OpenGL 投影参数;
- 执行模型图元的手动投影;
- 在
pMetaDC
上调用 GDI 绘图函数; - 关闭图元文件流;
- 将相应的缓冲区推送到剪贴板。
以下代码由菜单 Export/wmf... 启动的对话框窗口中的“Copy”按钮调用
BeginWaitCursor(); UpdateData(TRUE); // Get DC CDC *pDC = m_pDoc->GetView()->GetDC(); ASSERT(pDC); // Get view rect CRect rect; m_pDoc->GetView()->GetClientRect(&rect); rect.InflateRect(5,5); // 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); if(!hMetaDC) { AfxMessageBox("Unable to create MetaFile"); ReleaseDC(pDC); return; } // Get DC from handle CDC *pMetaDC = CDC::FromHandle(hMetaDC); ASSERT(pMetaDC); pMetaDC->SetMapMode(MM_TEXT); double ratio = 1; // Position / translation / scale from current view 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); // Get OpenGL parameters GLdouble modelMatrix[16]; GLdouble projMatrix[16]; GLint viewport[4]; glGetDoublev(GL_MODELVIEW_MATRIX,modelMatrix); glGetDoublev(GL_PROJECTION_MATRIX,projMatrix); glGetIntegerv(GL_VIEWPORT,viewport); // Start rendering via std GDI 2D drawing functions CSceneGraph3d *pScene = &m_pDoc->m_SceneGraph; for(int i=0;iNbObject();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, modelMatrix, projMatrix, viewport, m_ColorLine, m_Ratio, rect.Height()); else // The face mode (faces are z-sorted // according to their barycenter) ((CMesh3d *)pObject)->glDrawProjectFace(pMetaDC, modelMatrix, projMatrix, viewport, m_ColorLine, m_ColorFace, m_Ratio, rect.Height(), m_RatioNbFaces); } glPopMatrix(); // Close metafile HENHMETAFILE hMetaFile = CloseEnhMetaFile(hMetaDC); // Fill the clipboard (direct sent to wmf2eps or // any windows app such as Powerpoint) OpenClipboard(); EmptyClipboard(); SetClipboardData(CF_ENHMETAFILE,CopyEnhMetaFile(hMetaFile,NULL)); CloseClipboard(); // Cleanup DeleteEnhMetaFile(hMetaFile); ReleaseDC(pDC); EndWaitCursor(); }
以下代码对应于 3D 网格中的 glDrawProjectFace
函数
/******************************************** / 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], viewport[2],viewport[3]); TRACE(" model : %g\t%g\t%g\n",modelMatrix[0],modelMatrix[1], modelMatrix[2]); TRACE(" %g\t%g\t%g\n",modelMatrix[3],modelMatrix[4], modelMatrix[5]); TRACE(" %g\t%g\t%g\n",modelMatrix[6],modelMatrix[7], modelMatrix[8]); 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) ASSERT(pArray); 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 gluProject(xc,yc,zc, modelMatrix, projMatrix, viewport,&pArray[i].xc,&pArray[i].yc,&pArray[i].zc); // Project three vertices gluProject((double)pFace->v1()->x(), (double)pFace->v1()->y(), (double)pFace->v1()->z(), modelMatrix, projMatrix, viewport,&pArray[i].x1,&pArray[i].y1,&pArray[i].z1); gluProject((double)pFace->v2()->x(), (double)pFace->v2()->y(), (double)pFace->v2()->z(), modelMatrix, projMatrix, viewport,&pArray[i].x2,&pArray[i].y2,&pArray[i].z2); gluProject((double)pFace->v3()->x(), (double)pFace->v3()->y(), (double)pFace->v3()->z(), modelMatrix, projMatrix, viewport,&pArray[i].x3,&pArray[i].y3,&pArray[i].z3); // 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]) continue; else { pArray[i].m_Draw = 1; // yes, insert this triangle pArray[i].zc *= -1.0f; // back to front avl.Insert(pArray,i); // insert via sort NbFaceValid++; } } TRACE("ok\n"); // 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; for(i=avl.GetFirst(pArray); (AVLNULL != i) && nb < NbFacesToProcess; i=avl.GetNext(pArray),nb++) { // 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 pDC->Polygon(points,3); // Outline triangle pDC->MoveTo(points[0]); pDC->LineTo(points[1]); pDC->LineTo(points[2]); pDC->LineTo(points[0]); } TRACE("ok\n"); // Restore and cleanup pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush); delete [] pArray; }
图 6。 使用 PowerPoint 进行网格编辑,在 EMF/WMF 导出后,然后使用 Wolfgang Schulter 的共享软件 wmf2eps [2] 转换为 EPS。
导出到设备无关位图剪贴板
本节是对一篇先前的 文章的补充,该文章解释了如何从 OpenGL 程序中转储渲染图像。我收到了关于这篇先前文章的大量消息,其中指出了各种显卡和分辨率的错误。似乎glReadPixels
函数不够健壮,无法确保转储成功。因此,我们使用 CDC::GetPixel
GDI 函数将每个像素推送到 DIB 缓冲区。然后,我们使用与 Windows 剪贴板相关的直观序列 Open/Empty/SetData/Close。//********************************* // OnEditCopy //********************************* void CMeshView::OnEditCopy() { // Clean clipboard content, and copy the DIB. if(OpenClipboard()) { BeginWaitCursor(); // Snap (see below) CSize size; unsigned char *pixel = SnapClient(&size); // Image CTexture image; // Link image - buffer VERIFY(image.ReadBuffer(pixel,size.cx,size.cy,24)); // Cleanup memory delete [] pixel; EmptyClipboard(); SetClipboardData(CF_DIB,image.ExportHandle()); // short is better CloseClipboard(); EndWaitCursor(); } } // Hand-made client snapping // Slow and heavy.... but more robust than the // glReadPixels command (see previous article) unsigned char *CMeshView::SnapClient(CSize *pSize) { BeginWaitCursor(); // Client zone CRect rect; GetClientRect(&rect); CSize size(rect.Width(),rect.Height()); *pSize = size; ASSERT(size.cx > 0); ASSERT(size.cy > 0); // Alloc unsigned char *pixel = new unsigned char[3*size.cx*size.cy]; ASSERT(pixel != NULL); // Capture frame buffer 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); // slow but reliable 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; }
网格细分
细分曲面将平滑曲面定义为在一组基础网格上应用的连续细化步骤的极限。此类技术提供了许多好处,包括几何压缩、动画、编辑、可伸缩性和自适应渲染。本文提出的应用程序实现了 Charles Loop [1] 开发的 Loop 细分方案。它结合了每个面的 1:4 统一细分和几何过滤处理,确保了极限曲面的平滑性。图 7。 对网格 venus.wrl 应用两次统一 Loop 细分。每次连续的细分迭代使曲面更加平滑,从而消除了其多边形外观。左侧图像中可见的“星形”效果(来自线性 Gouraud 算法)也已被移除。
致谢
非常感谢 Mark J. Kilgard 和 Frederic Delhoume 实现初始的 PostScript 导出器。特别感谢 Gaspard Breton 使用 AVL 树实现了快速 z 排序算法。
参考文献
[1] Charles Loop。基于三角形的平滑曲面细分.
犹他大学数学系。
硕士论文。1987年。
[2] Wolfgang Schulter。
共享软件wmf2eps。版本 1.2(2000 年 4 月 2 日)http://www.wmf2eps.de.vu/
使用 Windows 95/98/NT/2000 PostScript 打印机驱动程序简单地将 WMF 转换为 EPS 文件。
[3] Mark J. Kilgard 和 Frederic Delhoume。
实现高质量的 OpenGL PostScript 输出.
http://reality.sgi.com/opengl/tips/Feedback.html