NeHe OpenGL第十七课:2D图像文字 2D图像文字: 在这一课中,你将学会如何使用四边形纹理贴图把文字显示在屏幕上。你将学会如何把256个不同的文字从一个256x256的纹理图像中分别提取出来,并为每一个文字创建一个显示列表,接着创建一个输出函数来创建任 意你希望的文字。 本教程由NeHe和Giuseppe D'Agata提供。 我知道每个人都或许厌恶字体。目前为止我写的文字教程不仅能显示文字,还能显示3D文字,有纹理贴图的文字,以及处理变量。但是当你将你的作品移植到不支持位图或是轮廓字体的机器上会发生什么事呢? 由于Giuseppe D'Agata我们有了另一篇字体教程。你还会问什么?如果你记得在第一篇字体教程中我提到使用纹理在屏幕上绘制文字。通常当你使用纹理绘制文字时你会调用你最喜欢的图像处理程序,选择一种字体 ,然后输入你想显示的文字或段落。然后你保存位图并把它作为纹理读入到你的程序里。对一个需要很多文字或是文字在不停变化的程序来说这么做效率并不高。 本教程只使用有一个纹理来显示任意256个不同的字符。记住平均一个字符只有16个像素宽,大概16个像素高。如果你使用标准的256x256的纹理那么很明显你可以放入交叉的16个文字(即一个X),且最多16行16列 。如果你需要一个更详细的解释:纹理是256个像素宽,一个字符是16个像素宽,256除以16得16:) 现在让我们来创建一个2D纹理字体demo!这课的程序基于第一课的代码。在程序的第一段,我们包括数学(math)和标准输入输出库(stdio)。我们需要数学库来使用正弦和余弦函数在屏幕上移动我们的文字,我们 需要标准输入输出库来保证在我们制作纹理前要使用的位图实际存在。 我们将要加入一个变量base来指向我们的显示列表。我们还加入texture[2]来保存我们将要创建的两个纹理。Texture 1将是字体纹理,texture 2将是用来创建简单3D物体的凹凸纹理。 GLuint base; // 绘制字体的显示列表的开始位置 GLfloat cnt1; // 字体移动计数器1 接下来是读取纹理代码。这跟前面纹理影射教程中的一模一样。 下面的代码同样对之前教程的代码改动很小。如果你不清楚下面每行的用途,回头复习一下。 int LoadGLTextures() // 载入位图(调用上面的代码)并转换成纹理 下一行十分重要。如果你用别的数字替换2将发生严重问题。再查一次!这个数字应该与你在设置TextureImages[ ]时的数字相匹配。 memset(TextureImage,0,sizeof(void *)*2); // 将指针设为 NULL if ((TextureImage[0]=LoadBMP("Data/Font.bmp")) && // 载入字体图像 另一十分重要,要检查两遍的行。我无法开始告诉你我收到多少email问“为什么我只看到一个纹理,或为什么我的纹理是全白的!?!”通常问题都出在这行。如果你用1替换2,那么将只创建一个纹理,第二个纹理将 显示为全白。如果你用3替换2,你的程序可能崩溃! 需要创建多少个纹理是个好主意,调用glGenTextures()一次,然后创建所有的纹理。把glGenTextures()放进循环是不明智的,除非你有自己的理由。 glGenTextures(2, &texture[0]); // 创建纹理 for (loop=0; loop<2; loop++) // 循环设置所有的纹理 [loop]->data); 下面的几行代码检查我们读取的位图数据是否在内存里。如果是,释放内存。注意我们还要检查并释放rgb图像记录。如果我们使用了3个不同的图像来创建纹理,我们要检查并释放3个rgb图像记录。 for (loop=0; loop<2; loop++) 现在我们将创建字体。我将以同样的细节来解释这段代码。这并没那么复杂,但是有些数学要了解,我知道不是每个人都喜欢数学。 GLvoid BuildFont(GLvoid) // 创建我们的字符显示列表 下面两个变量将用来保存字体纹理中每个字的位置。cx将用来保存纹理中水平方向的位置,cy将用来保存纹理中竖直方向的位置。 float cx; // 字符的X坐标 接着我们告诉OpenGL我们要建立256个显示列表。变量base将指向第一个显示列表的位置。第二个显示列表将是base+1,第三个是base+2,以此类推。 base=glGenLists(256); // 创建256个显示列表 现在我们开始循环。循环间创建所有的256个字符,每个存在它自己的显示列表里。 for (loop=0; loop<256; loop++) // 循环256个显示列表 下面的第一行或许看上去让人有点困惑。%符号表示loop除以16的余数。cx将我们通过字体纹理从左至右移动。你将注意到在后面的代码中我们用1减去cy从而从上到下而不是从下到上移动我们。%符号很难解释,但我 将尝试去解释。 我们真正关心的是(loop%16)。/16只是将结果转化为纹理坐标。所以如果loop等于16,cx将等于16/16的余数也就是0。但cy将等于16/16也就是1。所以我们将下移一个字符的高度,且我们将不往右移。如果 loop等于17,cx将等于17/16也就是1.0625。余数0.625也等于1/16。意味着我们将右移一个字符。cy将仍是1因为我们只关心小数点左边的数字。18/16将右移2个字符,但仍下移一个字符。如果loop是32,cx 将再次等于0,因为32除以16没有余数,但cy将等于2。因为小数点左边的数字现在是2,将下移2个字符。这么讲清楚吗? cx=float(loop%16)/16.0f; // 当前字符的X坐标 Ok。现在我们通过从字体纹理中依据cx和cy的值选择一个单独的字符创建了2D字体。在下面的行里我们给base的值加上loop,若不这么做,每个字都将建在第一个显示列表里。我们当然不想要那样的事发生,所以通 过给base加上loop,我们创建的每个字都被存在下个可用的显示列表里。 glNewList(base+loop,GL_COMPILE); //开始创建显示列表 现在我们已选择了我们要创建的显示列表,我们创建字符。这是通过绘制四边形,然后给他贴上字体纹理中的单个字符的纹理来完成的。 glBegin(GL_QUADS); // 使用四边形显示每一个字符 cx和cy应该保存一个从0.0到1.0的非常小的浮点数。如果cx和cy同时为0,下面第一行的代码将为:glTexCoord2f(0.0f,1-0.0f-0.0625f)。记得0.0625正是我们纹理的1/16,或者说是一个字符的宽/高。下 面的纹理坐标将是我们纹理的左下角。 因为我们的屏幕是以像素形式从0到639(宽)从0到479(高),我们既不需用浮点数也不用负数:) 我们设置正交投影屏幕的方式是,(0,0)将是屏幕的左下角,(640,480)是屏幕的右上角。x轴上0是屏幕的左边界,639是右边界。y轴上0时下便捷,479是上便捷。基本上我们避免了负坐标。对那些不在乎透视,更 愿意同像素而不是单元打交道的人来说更方便:) glTexCoord2f(cx,1-cy-0.0625f); // 左下角的纹理坐标 下一个纹理坐标现在是上个纹理坐标右边1/16(刚好一个字符宽)。所以这将是纹理的右下角。 glTexCoord2f(cx+0.0625f,1-cy-0.0625f); // 右下角的纹理坐标 第三个纹理坐标在我们的字符的最右边,但上移了纹理的1/16(刚好一个字符高)。这将是一个单独字符的右上角。 glTexCoord2f(cx+0.0625f,1-cy); // 右上角的纹理坐标 最后我们左移来设置字符左上角的最后一个纹理坐标。 glTexCoord2f(cx,1-cy); // 左上角的纹理坐标 最终,我们右移了10个像素,置于纹理的右边。如果我们不平移,文字将被绘制到各自的上面。由于我们的字体太窄,我们不想右移16个像素。如果那样的话,每个字之间将有很大间隔。只移动10个像素去除了间隔。 glTranslated(10,0,0); // 绘制完一个字符,向右平移16个单位 下面这段代码与我们在其它字体教程中用来在程序退出前释放显示列表的相同。所有自base开始的256个显示列表都将被销毁(这样做很好!)。 下一段代码将完成绘图。一切都几乎是新的,所以我将尽可能详细的解释每一行。一个小提示:很多都可加入这段代码,像是变量的支持,字体大小、间距的调整,和很多为恢复到我们决定打印前的状况所做的检查。 过Giuseppe D'Agata制作的位图,你会注意到有两个不同的字符集。第一个字符集是普通的,第二个是斜体的。如果set为0,第一个字符集被选中。若set为1则选择第二个字符集。 我们要做的第一件事是确保set的值非0即1。如果set大于1,我们将使它等于1。 if (set>1) // 如果字符集大于1 现在我们选择字体纹理。我们这么做是防止在我们决定往屏幕上输出东西时选择了不同的纹理。 glBindTexture(GL_TEXTURE_2D, texture[0]); // 绑定为字体纹理 现在我们禁用深度测试。我这么做是因为混合的效果会更好。如果你不禁用深度测试,文字可能会被什么东西挡住,或得不到正确的混合效果。如果你不打算混合文字(那样文字周围的黑色区域就不会显示)你可以启用 深度测试。 glDisable(GL_DEPTH_TEST); // 禁止深度测试 下面几行十分重要!我们选择投影矩阵。之后使用一个叫做glPushMatrix()的命令。glPushMatrix存储当前矩阵(投影)。有些像计算器的存储按钮。 glMatrixMode(GL_PROJECTION); // 选择投影矩阵 现在我们保存了投影矩阵,重置矩阵并设置正交投影屏幕。第一和第三个数字(0)表示屏幕的底边和左边。如果愿意我们可以将屏幕的左边设为-640,但如果不需要我们为什么要设负数呢。第二和第四个数字表示屏幕 的上边和右边。将这些值设为你当前使用的分辨率是明智的做法。我们不需要用到深度,所以我们将z值设为-1与1。 现在我们选择模型视点矩阵,用glPushMatrix()保存当前设置。然后我们重置模型视点矩阵以便在正交投影视点下工作。 在保存了透视参数,设置了正交投影屏幕后,现在我们可以绘制文字了。我们从移动到绘制文字的位置开始。我们使用 glTranslated()而不是glTranslatef()因为我们处理的是像素,所以浮点值并不重要。毕竟 ,你不可能用半个像素:) 下面这行选择我们要使用的字符集。如果我们想使用第二个字符集,我们在当前的显示列表基数上加上128(128时我们256个字符的一半)。通过加上128,我们跳过了头128个字符。 现在剩下的就是在屏幕上绘制文字了。我们同其它字体教程一样来完成这步。我们使用glCallLists()。strlen(string)是字符串的长度(我们想绘制多少字符),GL_UNSIGNED_BYTE意味着每个字符被表示为一 个无符号字节(一个字节是一个从0到255的值)。最后,字符串保存我们想打印的文字。 现在我们所要做的是恢复透视视图。我们选择投影矩阵并用glPopMatrix()恢复我们先前用glPushMatrix()保存的设置。用相反的顺序恢复设置很重要。 glMatrixMode(GL_PROJECTION); // 选择投影矩阵 现在我们选择模型视点矩阵,做相同的工作。我们使用glPopMatrix()恢复模型视点矩阵到我们设置正交投影显示之前。 glMatrixMode(GL_MODELVIEW); // 选择模型矩阵 最后,我们启用深度测试。如果你没有在上面的代码中关闭深度测试,你不需要这行。 glEnable(GL_DEPTH_TEST); // 启用深度测试 我们没有修改ReSizeGLScene(),所以我们直接跳到InitGL()。 我们跳到创建纹理的代码。如果由于某种原因创建纹理失败了,我们返回FALSE。这将让我们的程序知道发生了一个错误从而关闭程序。 if (!LoadGLTextures()) // 调用纹理载入子例程 如果没有错,我们跳到创建字体的代码。在创建字体时不会出什么错所以我们省略了错误检查。 BuildFont(); // 创建字符显示列表 现在我们做通常的GL设置。我们将背景色设为黑色,将深度清为1.0。我们选择一个深度测试模式和一个混合模式。我们启用平滑着色,最后启用2维纹理映射。 glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 黑色背景 下面这段代码将完成绘图。我们先绘制3D物体最后绘制文字,这样文字将显示在3D物体上面,而不会被3D物体遮住。我之所以加入一个3D物体是为了演示透视投影和正交投影可同时使用。 我们选择bumps.bmp纹理来创建简单的小3D物体。为了看见3D物体,我们往屏幕内移动5个单位。我们绕z轴旋转45度。这将使我们的四边形顺时针旋转45度,让我们的四边形看起来更像钻石而不是矩形。 在旋转45度后,我们让物体同时绕x轴和y轴旋转cnt1x30度。这使我们的物体象在一个点上旋转的钻石那样旋转。 glRotatef(cnt1*30.0f,1.0f,1.0f,0.0f); // 沿(1,1,0)轴旋转30度 我们关闭混合(我们希望3D物体看上去像实心的),设置颜色为亮白色。然后我们绘制一个单独的用了纹理映像的四边形。 glDisable(GL_BLEND); // 关闭混合 glRotatef(90.0f,1.0f,1.0f,0.0f); // 沿(1,1,0)轴旋转90度 在绘制完有纹理贴图的四边形后,我们开启混合并绘制文字。 glEnable(GL_BLEND); // 启用混合操作 我们使用同其它字体教程一样的生成很棒的颜色的代码。颜色会随着文字的移动而逐渐改变。 // 根据字体位置设置颜色 我们来绘制文字。我们仍然使用glPrint()。第一个参数是x坐标,第二个是y坐标,第三个("NeHe")是要绘制的文字,最后一个是使用的字符集(0-普通,1-斜体)。 glPrint(int((280+250*cos(cnt1))),int(235+200*sin(cnt2)),"NeHe",0); glColor3f(1.0f*float(sin(cnt2)),1.0f-0.5f*float(cos(cnt1+cnt2)),1.0f*float(cos(cnt1))); glColor3f(0.0f,0.0f,1.0f); glColor3f(1.0f,1.0f,1.0f); cnt1+=0.01f; // 增加计数器值 KillGLWindow(), CreateGLWindow()和WndProc()的代码都没有更改,所以我们跳过它们。 |
|