原文地址:http://www.videotutorialsrock.com/opengl_tutorial/draw_text/home.php
视频下载:http://www.videotutorialsrock.com/opengl_tutorial/draw_text/video.flv
文本格式:http://www.videotutorialsrock.com/opengl_tutorial/draw_text/text.php
源码下载:http://www.videotutorialsrock.com/opengl_tutorial/draw_text/drawtext.zip
OpenGL中绘制文本
有时候绘制文本相当重要。在OpenGL中绘制文本有几种方法,我将概述四种方法和其优缺点。
可以使用bitmap,并不是图片的那种,而是我还没有演示过的OpenGL中一种特定的结构。每个字符表示为一个bitmap。比特图中的每个像素包含了一个bit用来指定这个像素是否上色(1表示有颜色,0表示透明)。每帧你都需要向显卡发送字符bitmap。显卡之后会做3D变换,并将像素放在窗体的最上方。我并不喜欢这种方式。因为每一帧你都需要向显卡发送每个字符的比特图且数据量很大,所以很慢。这种方式同时也不太方便,你不可以缩放或者变换字符,在GLUT中使用glutBitmapCharacter中实现这种方法。相关文档在http://www.opengl.org/documentation/specs/glut/spec3/node75.html#SECTION000110000000000000000。再次声明,这种方法于技术上有很多缺点。
可以使用纹理表示字体。每个字符可以对应某个纹理的一部分,使用其中一部分的纹理,并使其他地方透明(我还没有演示如何实现)。每个字符需要绘制一个四边形,并建立纹理合适部分的映射关系。这个方法还将就,可以提供一定的方便比如在3D如何、在什么位置绘制你的字符。同时也比较快,但是字符同样不能伸缩,如果你放大太大就会看起来像素化了(pixelated)。
使用GL_LInes你可以在3D中绘制许多线条(虽然没有演示,不过你可以猜到用GL_LINES了)。这项技术比较快也可以伸缩和变换字符。但是,字符如果能覆盖一个区域而不是边缘的话将看起来更好看点。同时,使用一系列的线来描绘每个字符是枯燥和乏味的。你可以使用GLUT中的glutStrokeCharacter来绘制外围轮廓,相关文档在http://www.opengl.org/documentation/specs/glut/spec3/node75.html#SECTION000110000000000000000。
在3D中你可以绘制很多的多边形。这项技术也可以让我们绘制字符。甚至可以设定字符的3D深度,使得看上去是3D的而不是平的。但是这要比绘制线段和使用纹理慢些。同时,这要比弄明白如何使用一组线段来绘制字符更加繁琐。
我们如何绘制文本
在上述的四项技术中,我们使用最后一种。我已经将大部分的工作已经做好。使用开源的3D模型程序Blender,我使用“添加字符”特性对95个ASCII中的每个字符实现一个3D模块。我使用工具大大减少多边形的数量,使得每个字符大概只有40个多边形。我给每个字符一些3D的深度值,并将每个字符保存为独立的文件,并使用一个程序加载这些文件并输出到一个我自己设计的特别的文件中。然后,编写代码从文件中加载这些模块并使用t3dDraw2D和t2dDraw3D函数显示他们,这稍后介绍。
先将细节放一边,基本的思想是:现在有一个文件,包含了所有可能的针对不同字符的3D多边形。t3dDraw2D和t3dDraw3D函数负责绘制合适的三角形。为了使这些绘制尽可能的快,这些函数本身使用了一些我还没有提过的OpenGL技术。
究竟这些函数有多快呢?2D绘制大概每个字符绘制40个三角形。显卡每秒可以绘制几百万个三角形。因此,我们能估算出这个函数每秒绘制1,000,000/40=25,000个字符,这我在一个小的测试里拼凑了这么多。因此,如果你并不是绘制大量的字符,这已经足够了,但是如果你确实要绘制很多,你可能会转而使用glutStrokeCharacter函数绘制线段而不是多边形。
源代码
我们希望将3D字符放在一个正方形的四个边上,程序看起来如下图:
现在来看源代码:
//Computes a scaling value so that the strings
float computeScale(const char* strs[4]) {
float maxWidth = 0;
for(int i = 0; i < 4; i++) {
float width = t3dDrawWidth(strs[i]);
if (width > maxWidth) {
maxWidth = width;
}
}
return 2.6f / maxWidth;
}
方块的每个边长度为3。我们想让最长的字符串占据方块的2.6个单位,因此使用computeScale函数决定我们缩放文本的缩放系数。对每个字符串使用t3dDrawWidth来决定绘制字符串的长度。我们使用2.6除以最大的宽度作为缩放的系数。
//The four strings that are drawn
const char* STRS[4] = {"Video", "Tutorials", "Rock", ".com"};
数组STRS包含了我们将要绘制的字符串.
void cleanup() {
t3dCleanup();
}
cleanup函数调用text3d.h文件中的t3dCleanup函数以释放被文本绘制申请的内存。
void initRendering() {
//...
t3dInit();
}
在我们的initRendering函数中,我们需要设置一些绘制3D文本的东西。具体来说,我们需要从文件"charset"中加载每个字符的三角形的位置。因此我们调用t3DInit(),这也是包含在text3d.h文件中。
void drawScene() {
//...
//Draw the strings along the sides of a square
glScalef(_scale, _scale, _scale);
glColor3f(0.3f, 1.0f, 0.3f);
for(int i = 0; i < 4; i++) {
glPushMatrix();
glRotatef(90 * i, 0, 1, 0);
glTranslatef(0, 0, 1.5f / _scale);
t3dDraw3D(STRS[i], 0, 0, 0.2f);
glPopMatrix();
}
//...
}
这里是我们绘制3D文本的地方。首先,我们缩放到合适的比例,然后对每个字符串,我们移动立方体的每个合适的边并使用t3dDtraw3D将字符串画出。
t3dDtraw3D函数有5个参数。这里只有4个,省略了第5个(使用默认值)。如果你看下text3d.h文件安,你可以看到这是在c++完成的:
void t3dDraw3D(std::string str,
int hAlign, int vAlign,
float depth,
float lineHeight = 1.5f);
我们设置lineHeight为1.5f,告诉编译器对于最后一个参数如果省略就使用1.5的默认值。
第一个参数是将要绘制的字符串。第二个是字符串的水平对齐方式,负值是左对齐,0是中间对齐,正值是右对齐。第三个参数是字符串的垂直对齐,负值表示上对齐,0表示中间对齐,正值表示下对齐。(如果你想要绘制多行字符,使用换行符。)第四个参数是字符的3D深度值乘以字体的高度。第五个参数是行的高度乘以字体的高度。如果我们使用多行来绘制文本,这可以用来指明行间距。如果不是,就使用默认的1.5。
如果希望绘制文本不需要深度值,这样所有的多边形就在同一个平面上,可以调用t3dDraw2D函数,除了省略了文本的深度值以外参数相同。因为这样需要绘制的多边形就少了,所以要快些,但是没有好看的3D效果。
int main(int argc, char** argv) {
//...
_scale = computeScale(STRS);
//...
}
最后,在我们的主函数中,调用computeScale计算出伸缩文本的拉伸系数。
好了,我们在OpenGL实现了一些3D的文本!
练习:
- 改变程序,实现一个旋转的字符,从小写的'a'开始改变到用户按下的字符。使用GLUT的stroke函数(gluStrokeCharacter和glutStrokeWidth)而不用t3dDraw3D绘制。你或许需要GLUT的在线帮助了解函数如何工作。
- 修改程序从一个旋转的方块到一个旋转的八角形,每个面有一个8个字符的单词(如:spinning)。使用2D文本绘制(t3dDraw2D)而不使用3D文本绘制。