c++ opengl 三维图形中显示文字_(旧)在OpenGL上设置字体和显示文字

ace25f42ecf0a4d640d0bfc29661a468.png

任何一个DEMO、仿真项目、游戏,都少不了文字这种媒体。这不可不说是对图形视觉媒体的补充——我们还有一些无法超越文字来向观众表达的心事,或是补充说明,或是感悟,或是感激。—— ZwqXin.com

一般的文字不属于图形渲染部分,而属于用户界面部分,这在游戏引擎中看或许一目了然,但是在底层的图形渲染API——OPENGL或D3D中,文字的显示“并不是必须”,但它是多么深深地被需要着口牙。所以,把字体设置、文字显示作为一种图形学技术而非单纯的完全我属或他属,我是这么想的。(同样,拾取也算是这样吧?[乱弹OpenGL选择-拾取机制Ⅰ])

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/opengl/opengl-font-setting-showing.html

怎么表达文字呢?在OpenGL中,我们没有什么现成的东西可用,但确实有办法让我们“得到这种技术”。让我最记忆深刻的是NEHE的两三篇教程(貌似都是十几课吧),讲述的就是今天的这个主题。可以到NEHE网站或者在DANCINGWIND的中文翻译(见[搜集的优良OpenGL教程] )看看~。

而我所知道的这三种方法,前两种应该就是来自那里吧(记得~~),当时要努力完成课程DEMO,于是胡胡混混地就把相应的那两三课学了,而且把它的文字显示方法应用到自己的程序中(还经历了一段艰辛的探索史...连我当时的MyFont类也记录了这份小辛酸,现在看来,是因为当时的知识水平不够理解吧)。然后后来又一个课程DEMO,老师后来觉得我应该“写中文”,于是我又去探索中文字体显示方法了,并在一个开源DEMO中找到,分析代码段后就拿来主义了,至昨不曾好好考究——这就是我所知的第三种方法。

三种方法都是三步曲:在初始化的时候“创建字体”[Build],在渲染阶段“应用字体”(显示文字)[Print],在程序结束或不再需要文字显示的时候“销毁字体”[kill]。其中前两步比较重要,着重讨论讨论哈~

1. 常规的屏幕字体打印(NormalFont)

应用得比较广,大名鼎鼎的AZURE以前的DEMO就是用这个的。NEHE教程中作为首次出现的字体显示方法,介绍应该比较全面,大家想仔细了解的话请务必从上面网址进入学习(当然还有因为我理解不透不敢乱讲的缘由)。

  1. /一般的英语字体打印
  2. void MyFont::BuildGLFont(int fontHeight)
  3. {
  4. HDC hDC =::GetDC(HWND_DESKTOP); //就是这里搞晕了半晚
  5. int tFontHeight = -1 * fontHeight;
  6. NormalFontBase = glGenLists(96); // Storage For 96 Characters
  7. HFONT font = CreateFont( -tFontHeight, // Height Of Font
  8. 0, // Width Of Font
  9. 0, // Angle Of Escapement
  10. 0, // Orientation Angle
  11. FW_BOLD, // Font Weight
  12. TRUE, // Italic
  13. FALSE, // Underline
  14. FALSE, // Strikeout
  15. ANSI_CHARSET, // Character Set Identifier
  16. OUT_TT_PRECIS, // Output Precision
  17. CLIP_DEFAULT_PRECIS, // Clipping Precision
  18. ANTIALIASED_QUALITY, // Output Quality
  19. FF_DONTCARE|DEFAULT_PITCH, // Family And Pitch
  20. "Georgia"); // Font Name
  21. HFONT oldfont = (HFONT)SelectObject(hDC, font); // Selects The Font We Want
  22. wglUseFontBitmaps(hDC, 32, 96, NormalFontBase); // Builds 96 Characters Starting At Character 32
  23. SelectObject(hDC, oldfont); // Selects The Font We Want to return to
  24. DeleteObject(font); // Delete The Font
  25. SetBkMode(hDC,TRANSPARENT);
  26. NormalFont = true;
  27. }

其中bUild的时候首先用到的是GDI的CreateFont函数创建字体——这应该是比较常用的方法,设置了关于字体的一切并选入字体后,有一步重要的操作:wglUseFontBitmaps。

  1. wglUseFontBitmap
  2. 为当前选中的GDI字体创建一组OpenGL显示列表位图字体
  3. BOOL wglUseFontBitmap(HDC hDC, DWORD dwFirst, DWORD dwCount, DWORD dwListBase);
  4. 参数
  5. hDC
  6. 设备环境句柄
  7. dwFirst
  8. 用于创建显示列表字体的第一个字符的ASCII值
  9. dwCount
  10. 字符数
  11. dwListBase
  12. 第一个显示列表的名称
  13. 返回值
  14. 成功返回TRUE,否则返回FALSE

输入为DC,32,96以及创建的96个新显示列表的base列表。函数绘制从ASCII码为32-128的字符进入显示列表,依赖OPENGL显示列表的高速显示能力(直接从硬件拿存储区),可以方便进行字符的切换。

在Print过程中,调用glCallLists就能调动起这些列表了,但是怎么决定具体要“调动”哪些字母呢(96个之中)?

  1. void MyFont::PrintGLText(GLint x, GLint y, const char *string, ...)
  2. char text[256];
  3. va_list pArguments;
  4. if (string == NULL)
  5. return;
  6. va_start(pArguments, string);
  7. vsprintf(text, string, pArguments);
  8. va_end(pArguments);
  9. glPushAttrib(GL_LIST_BIT | GL_CURRENT_BIT | GL_ENABLE_BIT | GL_LIGHTING_BIT);
  10. glDisable(GL_DEPTH_TEST);
  11. glDisable(GL_LIGHTING);
  12. glDisable(GL_TEXTURE_2D);
  13. glColor4f(mColor[0], mColor[1], mColor[2], mColor[3]);
  14. glWindowPos2i(x, y);
  15. glListBase(NormalFontBase - 32);
  16. glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
  17. glPopAttrib();
  18. }

首先看函数形式——printf形式,若想有个详细了解,可到这里看看。简单来说,就是C时代的可变参数列。va_start - vsprintf - (va_arg)- va_end这套机制就是为了把可变参数列的内容,通过va_list (char*指针)一个一个从栈中取出来赋予他者——我们的glCallLists所要接受的所有“具体字符”,通过base为基础的索引快速寻觅而取得对应ASCII字符的字体信息(实际是位图字体),并依照期望使其形成为“具体字符串”印入屏幕。

另外着重介绍的是我所添加的两个优化——它们贯穿三种文字显示方法之中。

其一是glPushAttrib,它与glPopAttrib配合,保证了其之间的OPENGL状态设置的独立性,使其不影响该代码逻辑的前后的具体渲染状态。当然参数取GL_ALL_ATTRIB_BITS是保险点,但只要你弄清楚自己的需要,像上面这样给予特定的状态作为参数效率会更高。恩,颜色,光照,深度测试,混合……选择与当前方法最匹配的状态而没有对状态机的后顾之忧,如同文字本身一样——作为对象完全独立于图形渲染“模块”。

另一是glWindowPos2i(x, y),按《OpenGL编程指南》第8章所说,它取代我们以前用的glRasterPos2i,不再让作为描绘对象的物体承受模型-视图-投影变换之苦[乱弹OpenGL中的矩阵变换(上)] ,而是直接独立到OPENGL世界的出口——屏幕坐标系,如GDI般用窗口坐标(根据屏幕像素数)来描述文字的起点位置。这同样是赋予文字的独立性,而且意义重大——可知道,当时我用glRasterPos2i多么狼狈,好难才让文字不在场景“里面”乱窜。

在具体应用中,在初始化调用BuildGLFont.,在渲染阶段调用PrintGLText。譬如:

  1. //CMAINFRAME
  2. MyFont mFont;
  3. //初始化:
  4. mFont.BuildGLFont(25);//25是字体字高,控制字体大小
  5. //渲染阶段(RenderGLScene)
  6. mFont.PrintGLText(530, 710, "http://www.Zwqxin.com - My 3D Graphics");
  7. //将在坐标X = 530, Y =710位置开始绘制文字。对1024*768大小的渲染窗口中,即在右上角

注意,OpenGL窗口坐标系的原点在窗口的左下角,横坐标为X,竖坐标为Y,最大值在右上角。(同见[乱弹OpenGL选择-拾取机制Ⅱ] )

676634c3ad5cf25212ccf5efe3eef3c4.png


浏览一下效果,第一行就是了,因为我默认用的是Georgia字体(应该一般人电脑都有),所以很漂亮

接下来会谈及其余两种方法,并比较之。真正的纹理文字是怎样弄的呢?怎样让中文字体乖乖显示?什么时候用哪种方法?我集成的MyFont类是怎样个怪样?听下回分解:

在OpenGL上设置字体和显示文字(下)​www.zwqxin.com

本篇紧随上篇,继续说一下鄙人所了解的在OpenGL进行文字显示的方法,也方便不知从哪个次元来到这里的学习者提供一点这方面的信息。上一篇见:[在OpenGL上设置字体和显示文字(上)] ——ZwqXin.com

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/opengl/opengl-font-setting-showing-2.html

2. 纹理字体

最近接触Irrlicht引擎,里面的GUI模块涉及的字体设置就是用了这种纹理字体的方法。事实上,上篇的方法最后多少了涉及到了位图字体,但是这里所说的,是直接从一张“写着各个英文字符和其他常用字符”的纹理上,按对此纹理上的图案“结构”的先验知识而获取字符对应的“纹理部分”,显示到屏幕上。

换句话来说,这就是真正的纹理贴图了,因此必须结合一张特定设计的“字体纹理”。要显示一个字符,需要你绘制一个一定大小的矩形(这个长度值相当于之前的字体高度——它控制最后出来的文字的大小),然后找到你所需字符在该纹理上的纹理坐标,作为索引检索出纹理上的对应字符部分的小纹理,贴到该矩形上。

  1. //从字体集纹理中取出的字符
  2. void MyFont::BuildTextureFont(GLuint fonttex, int fontHeight, int screenWidth, int screenHeight)
  3. {
  4. TextureFontFont = fonttex;
  5. float cx; // Holds Our X Character Coord
  6. float cy; // Holds Our Y Character Coord
  7. glEnable(GL_TEXTURE_2D);
  8. TextureFontBase = glGenLists(256); // Creating 256 Display Lists
  9. glBindTexture(GL_TEXTURE_2D, TextureFontFont); // Select Our Font Texture
  10. for(int loop=0; loop<256; loop++) // Loop Through All 256 Lists
  11. {
  12. cx=float(loop%16)/16.0f; // X Position Of Current Character
  13. cy=float(loop/16)/16.0f; // Y Position Of Current Character
  14. glNewList(TextureFontBase + loop, GL_COMPILE); // Start Building A List
  15. glBegin(GL_QUADS); // Use A Quad For Each Character
  16. glTexCoord2f(cx,1-cy-0.0625f); // Texture Coord (Bottom Left)
  17. glVertex2i(0,0); // Vertex Coord (Bottom Left)
  18. glTexCoord2f(cx+0.0625f,1-cy-0.0625f); // Texture Coord (Bottom Right)
  19. glVertex2i(fontHeight, 0); // Vertex Coord (Bottom Right)
  20. glTexCoord2f(cx+0.0625f,1-cy); // Texture Coord (Top Right)
  21. glVertex2i(fontHeight,fontHeight); // Vertex Coord (Top Right)
  22. glTexCoord2f(cx,1-cy); // Texture Coord (Top Left)
  23. glVertex2i(0, fontHeight); // Vertex Coord (Top Left)
  24. glEnd(); // Done Building Our Quad (Character)
  25. glTranslated(fontHeight,0,0); // Move To The Right Of The Character
  26. glEndList(); // Done Building The Display List
  27. } // Loop Until All 256 Are Built
  28. glDisable(GL_TEXTURE_2D);
  29. ScreenWidth = screenWidth;
  30. ScreenHeight = screenHeight;
  31. TextureFont = true;
  32. }

这在BUILD阶段就做的必要是没有的,但是一次过导入纹理中256个字符,生成256个小纹理,并在每帧都选择对应的纹理索引检索纹理,且每个字符如此——这样的重复不变而低效的事情,还是由OpenGL的显示列表技术来做比较好——一次过在初始化时做好,放入显示列表。在 PRINT阶段只要调用显示列表就好。关于显示列表,eastcowboy在他的OPENGL入门学习中详细提及过,有兴趣的朋友可看看他的文章:OpenGL入门学习——第八课-使用显示列表 。比起最时兴的VBO,显示列表在重复劳动上还是有一定优势的额~

最后的结果是按glTranslated进行排列的256个具有纹理字符的矩形。为什么要得到渲染窗口的大小呢?因为这些矩形要保证在屏幕最前方,就应该让它突破矩阵变换啊。在上篇[在OpenGL上设置字体和显示文字(上)] 也提过glWindowPos2i(x, y),但这里对实际的矩形它不是很适用。因此我还是选择原来的路子,来给予文字以独立于图形的属性——在屏幕最前而位置只由屏幕坐标XY决定。方法就是,或许很多人也熟悉的,glOrtho正交投影变换。因此,屏幕大小(这里指渲染窗口的大小)是需要的。

  1. void MyFont::PrintTextureText(GLint x, GLint y, char *string, int TextureSet)
  2. {
  3. if (TextureSet > 1)TextureSet = 1;
  4. if (TextureSet < 0)TextureSet = 0;
  5. glPushAttrib(GL_CURRENT_BIT | GL_LIGHTING_BIT | GL_ENABLE_BIT| GL_LIST_BIT);
  6. glDisable(GL_LIGHTING);
  7. glEnable(GL_TEXTURE_2D);
  8. glBindTexture(GL_TEXTURE_2D, TextureFontFont); // Select Our Font Texture
  9. glDisable(GL_DEPTH_TEST); // Disables Depth Testing
  10. glEnable(GL_BLEND);
  11. glBlendFunc(GL_SRC_ALPHA,GL_ONE);
  12. glColor4f(mColor[0], mColor[1], mColor[2], mColor[3]);
  13. glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
  14. glPushMatrix(); // Store The Projection Matrix
  15. glLoadIdentity(); // Reset The Projection Matrix
  16. glOrtho(0,ScreenWidth,0,ScreenHeight,-1,1); // Set Up An Ortho Screen
  17. glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
  18. glPushMatrix(); // Store The Modelview Matrix
  19. glLoadIdentity(); // Reset The Modelview Matrix
  20. glTranslated(x,y,0); // Position The Text (0,0 - Bottom Left)
  21. glListBase(TextureFontBase-32 + (128 * TextureSet));// Choose The Font Set (0 or 1)
  22. glCallLists(strlen(string),GL_UNSIGNED_BYTE,string);// Write The Text To The Screen
  23. glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
  24. glPopMatrix(); // Restore The Old Projection Matrix
  25. glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
  26. glPopMatrix(); // Restore The Old Projection Matrix
  27. glDisable(GL_BLEND);
  28. glEnable(GL_DEPTH_TEST); // Enables Depth Testing
  29. glDisable(GL_TEXTURE_2D);
  30. glPopAttrib();
  31. }

TextureSet选择字符集,因为字符纹理里面每个字符对应两种字体,用0/1选择。glPushAttrib的作用前面说过了。之后我们用glPushMatrix保存当前的投影/模型视图矩阵,在弄好一切好就马上切换回去。弄什么呢?新的坐标变换。文字(glCallLists绘制)是脱离我们OPENGL图形世界的,因此单独给予它一套变换——在屏幕最前方且不受视觉透视影响。glOrtho(0,ScreenWidth,0,ScreenHeight,-1,1)这样的投影变换设置配合glTranslated这样的模型变换就能满足我的要求了。因为创建的正交投影是与渲染窗口大小一致的,所以glTranslated的X,Y的单位与像素pixel对应——这不也就是GDI那种设置方式了么哈。

具体应用:

  1. //CMAINFRAME
  2. MyFont mFont;
  3. //初始化:
  4. mFont.BuildTextureFont(FontTextureID, 25, VB_WIDTH, VB_HEIGHT);
  5. //25是字体字高,控制字体大小 ,FontTextureID字体纹理的纹理ID
  6. //渲染阶段(RenderGLScene)
  7. mFont.PrintTextureText(790,645,"Font Test",1);
  8. //将在坐标X = 790, Y =645位置开始绘制文字。对1024*768大小的渲染窗口中,
  9. //即在右上角偏下

3.GDI字体

事实上第一种方法也可以说是GDI的方法,但是这里更明显,结合GDI字体创建和位图创建。而且它不涉及显示列表。它是在渲染时动态创建包容文字的设备相关位图(具体可参考我的一篇旧文[认识HBITMAP与Bmp操作(整内存拷贝版)] ),再把此位图定位而成的。因此,它是很慢的~(抽)。但是为了中文字体,一点点的性能损失算得了什么呢。

  1. ///GDI, 位图字体 可设中文字体
  2. void MyFont::BuildGDIFont(LPCTSTR lpszFacename, int fontWeights, int fontHeight)
  3. {
  4. int tfontHeight = -1 * fontHeight;
  5. hGDIFont = CreateFont(tfontHeight, 0, 0, 0, fontWeights, 0, 0, 0, GB2312_CHARSET,
  6. 0, 0, 0, FF_MODERN, lpszFacename);
  7. GDIFont = true;
  8. }

BUILD部分就不多说了,就是CreateFont~~fontWeights表示字体的重量,在0~900内可选,直接设置FW_BOLD之类的也行,这里给出这个参数不过是多给它一些可控性而已。首参数是字体,譬如可以是"黑体","宋体"等等,也可以是英文字体。当然这里应该是取你电脑的字库里的字体,所以考虑程序的通用性,别搞些另类的字体或只有自己有的字体~GB2312_CHARSET你该知道啦哈。

  1. void MyFont::PrintfChtext(int x, int y, LPCTSTR lpszText)
  2. {
  3. CBitmap bitmap;
  4. BITMAP bm;
  5. SIZE size;
  6. HDC MDC = ::CreateCompatibleDC(NULL);
  7. SelectObject(MDC, hGDIFont);
  8. ::GetTextExtentPoint32(MDC,lpszText,strlen(lpszText),&size);
  9. bitmap.CreateBitmap(size.cx, size.cy, 1, 1, NULL);
  10. HBITMAP oldBmp=(HBITMAP)SelectObject(MDC,bitmap);
  11. SetBkColor (MDC, RGB(0, 0, 0));
  12. SetTextColor(MDC, RGB(255, 255, 255));
  13. TextOut(MDC, 0, 0, lpszText, strlen(lpszText));
  14. bitmap.GetBitmap(&bm);
  15. size.cx = (bm.bmWidth + 31) & (~31);
  16. int bufsize = size.cy * size.cx;
  17. struct {
  18. BITMAPINFOHEADER bih;
  19. RGBQUAD col[2];
  20. }bic;
  21. BITMAPINFO *binf = (BITMAPINFO *)&bic;
  22. binf->bmiHeader.biSize = sizeof(binf->bmiHeader);
  23. binf->bmiHeader.biWidth = bm.bmWidth;
  24. binf->bmiHeader.biHeight = bm.bmHeight;
  25. binf->bmiHeader.biPlanes = 1;
  26. binf->bmiHeader.biBitCount = 1;
  27. binf->bmiHeader.biCompression = BI_RGB;
  28. binf->bmiHeader.biSizeImage = bufsize;
  29. UCHAR* Bits = new UCHAR[bufsize];
  30. ::GetDIBits(MDC,bitmap, 0, bm.bmHeight, Bits, binf, DIB_RGB_COLORS);
  31. glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  32. //glRasterPos2i(x, y);
  33. glWindowPos2i(x, y);
  34. glBitmap(size.cx, size.cy, 0, 0, 0, 0, Bits);
  35. delete Bits;
  36. SelectObject(MDC, oldBmp);
  37. ::DeleteDC(MDC);
  38. }
  39. void MyFont::PrintGDIText(GLint x, GLint y, CString str)
  40. {
  41. glLoadIdentity();
  42. glPushAttrib(GL_CURRENT_BIT | GL_LIGHTING_BIT);
  43. glDisable(GL_TEXTURE_2D);
  44. glDisable(GL_LIGHTING);
  45. glColor4f(mColor[0], mColor[1], mColor[2], mColor[3]);
  46. PrintfChtext (x, y, str);
  47. glPopAttrib();
  48. }

glPushAttrib和glWindowPos2i的意义不多说了。看主体函数PrintfChtext。几个GDI函数:

  • GetTextExtentPoint32用当前所选字体来计算字符串尺寸,按逻辑单位计算的高和宽都没有考虑裁剪取的情况。
  • CreateBitmap创建单位色位图。

函数所做的是依据字符串大小建立一张单色位图,把该位图信息存入UCHAR数组Bits内。类似的操作在[认识HBITMAP与Bmp操作(整内存拷贝版)] 也谈过,所以细节部分譬如为什么要取宽度位8倍数等等就略过。重点是要把数组Bits交给谁。恩,glBitmap函数道出了一切。

OpenGL里除了几何对象(点、线、多边形)和纹理图像对象外,还有一种不常用的对象,位图Bitmap。一般来说这样的位图是灰度的,也就是位图矩块内每个像素只有1BIT的信息——0 OR 1。这对文字来说正好,因为文字就是黑色(/透明)背景中的白色部分,黑白分明。颜色可以在绘制位图后用glColor设置嘛。glBitmap函数就是用来根据数据显示位图的。而该位图内可以说是被打印了GDI文字上去(用TextOut),于是最后在屏幕上的还是位图文字,且可以用glWindowPos2i调节起始点位置。就这样,完成的GDI文字从基于DC的GDI环境,显示到基于RC的OPenGL环境上。可喜可贺。

具体应用:

  1. //CMAINFRAME
  2. MyFont mFont;
  3. //初始化:
  4. mFont.BuildGDIFont("宋体", FW_NORMAL, 25);
  5. //25是字体字高,控制字体大小 ;FW_NORMAL常态重的字体
  6. //渲染阶段(RenderGLScene)
  7. mFont.PrintGDIText(840, 675, "抖动波演示[D]");
  8. //将在坐标X = 840, Y =675位置开始绘制文字。对1024*768大小的渲染窗口中,
  9. //即在右上角偏下

于是,我所知道的就这么多了。阁下若有更好的建议,........求指教!

效果(分别上到下对应方法1,3,2):

676634c3ad5cf25212ccf5efe3eef3c4.png

最后是MyFont类了,加上纹理字体的那张特制字体纹理(2011.2.结构更新ZWFont)。阁下若有更好的改进,........求指教!

ZWFont类byZwqXin.zip​www.zwqxin.com

2009-8-5/2009-8-6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值