• OpenGL------在Windows系统中显示文字


    增加了两个文件,showline.c, showtext.c。分别为第二个和第三个示例程序的main函数相关部分。
    在ctbuf.h和textarea.h最开头部分增加了一句#include <stdlib.h>
    附件中一共有三个示例程序:
    第一个,飘动的“曹”字旗。代码为:flag.c, GLee.c, GLee.h
    第二个,带缓冲的显示文字。代码为:showline.c, ctbuf.c, ctbuf.h, GLee.c, GLee.h
    第三个,显示歌词。代码为:showtext.c, ctbuf.c, ctbuf.h, textarea.c, textarea.h, GLee.c, GLee.h
    其中,GLee.h和GLee.c可以从网上下载,因此这里并没有放到附件中。在编译的时候应该将这两个文件和其它代码文件一起编译。

    本课我们来谈谈如何显示文字。
    OpenGL并没有直接提供显示文字的功能,并且,OpenGL也没有自带专门的字库。因此,要显示文字,就必须依赖操作系统所提供的功能了
    各种流行的图形操作系统,例如Windows系统和Linux系统,都提供了一些功能,以便能够在OpenGL程序中方便的显示文字。
    最常见的方法就是,我们给出一个字符,给出一个显示列表编号,然后操作系统由把绘制这个字符的OpenGL命令装到指定的显示列表中。当需要绘制字符的时候,我们只需要调用这个显示列表即可。
    不过,Windows系统和Linux系统,产生这个显示列表的方法是不同的(虽然大同小异)。作为我个人,只在Windows系统中编程,没有使用Linux系统的相关经验,所以本课我们仅针对Windows系统。

    OpenGL版的“Hello, World!”
    写完了本课,我的感受是:显示文字很简单,显示文字很复杂。看似简单的功能,背后却隐藏了深不可测的玄机。
    呵呵,别一开始就被吓住了,让我们先从“Hello, World!”开始。
    前面已经说过了,要显示字符,就需要通过操作系统,把绘制字符的动作装到显示列表中,然后我们调用显示列表即可绘制字符。
    假如我们要显示的文字全部是ASCII字符,则总共只有0到127这128种可能,因此可以预先把所有的字符分别装到对应的显示列表中,然后在需要时调用这些显示列表。
    Windows系统中,可以使用wglUseFontBitmaps函数来批量的产生显示字符用的显示列表。函数有四个参数:
    第一个参数是HDC,学过Windows GDI的朋友应该会熟悉这个。如果没有学过,那也没关系,只要知道调用wglGetCurrentDC函数,就可以得到一个HDC了。具体的情况可以看下面的代码。
    第二个参数表示第一个要产生的字符,因为我们要产生0到127的字符的显示列表,所以这里填0。
    第三个参数表示要产生字符的总个数,因为我们要产生0到127的字符的显示列表,总共有128个字符,所以这里填128。
    第四个参数表示第一个字符所对应显示列表的编号。假如这里填1000,则第一个字符的绘制命令将被装到第1000号显示列表,第二个字符的绘制命令将被装到第1001号显示列表,依次类推。我们可以先用glGenLists申请128个连续的显示列表编号,然后把第一个显示列表编号填在这里。
    还要说明一下,因为wglUseFontBitmaps是Windows系统特有的函数,所以在使用前需要加入头文件:#include <windows.h>。
    现在让我们来看具体的代码:

    #include <windows.h>
    
    // ASCII字符总共只有0到127,一共128种字符
    #define MAX_CHAR       128
    
    void drawString(const char* str) {
        static int isFirstCall = 1;
        static GLuint lists;
    
        if( isFirstCall ) { // 如果是第一次调用,执行初始化
                            // 为每一个ASCII字符产生一个显示列表
            isFirstCall = 0;
    
            // 申请MAX_CHAR个连续的显示列表编号
            lists = glGenLists(MAX_CHAR);
    
            // 把每个字符的绘制命令都装到对应的显示列表中
            wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists);
        }
        // 调用每个字符对应的显示列表,绘制每个字符
        for(; *str!=''; ++str)
            glCallList(lists + *str);
    }

    显示列表一旦产生就一直存在(除非调用glDeleteLists销毁),所以我们只需要在第一次调用的时候初始化,以后就可以很方便的调用这些显示列表来绘制字符了。
    绘制字符的时候,可以先用glColor*等指定颜色,然后用glRasterPos*指定位置,最后调用显示列表来绘制。

    void display(void) {
        glClear(GL_COLOR_BUFFER_BIT);
    
        glColor3f(1.0f, 0.0f, 0.0f);
        glRasterPos2f(0.0f, 0.0f);
        drawString("Hello, World!");
    
        glutSwapBuffers();
    }

    指定字体
    在产生显示列表前,Windows允许选择字体。
    我做了一个selectFont函数来实现它,大家可以看看代码。

    void selectFont(int size, int charset, const char* face) {
        HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
            charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
            DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face);
        HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
        DeleteObject(hOldFont);
    }
    
    void display(void) {
        selectFont(48, ANSI_CHARSET, "Comic Sans MS");
    
        glClear(GL_COLOR_BUFFER_BIT);
    
        glColor3f(1.0f, 0.0f, 0.0f);
        glRasterPos2f(0.0f, 0.0f);
        drawString("Hello, World!");
    
        glutSwapBuffers();
    }

    最主要的部分就在于那个参数超多的CreateFont函数,学过Windows GDI的朋友应该不会陌生。没有学过GDI的朋友,有兴趣的话可以自己翻翻MSDN文档。这里我并不准备仔细讲这些参数了,下面的内容还多着呢:(
    如果需要在自己的程序中选择字体的话,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。
    效果如图:

    显示中文
    原则上,显示中文和显示英文并无不同,同样是把要显示的字符做成显示列表,然后进行调用。
    但是有一个问题,英文字母很少,最多只有几百个,为每个字母创建一个显示列表,没有问题。但是汉字有非常多个,如果每个汉字都产生一个显示列表,这是不切实际的。
    我们不能在初始化时就为每个字符建立一个显示列表,那就只有在每次绘制字符时创建它了。当我们需要绘制一个字符时,创建对应的显示列表,等绘制完毕后,再将它销毁。
    这里还经常涉及到中文乱码的问题,我对这个问题也不甚了解,但是网上流传的版本中,使用了MultiByteToWideChar这个函数的,基本上都没有出现乱码,所以我也准备用这个函数:)
    通常我们在C语言里面使用的字符串,如果中英文混合的话,例如“this is 中文字符.”,则英文字符只占用一个字节,而中文字符则占用两个字节。用MultiByteToWideChar函数,可以转化为所有的字符都占两个字节(同时解决了前面所说的乱码问题:))。
    转化的代码如下:

    / 计算字符的个数
    // 如果是双字节字符的(比如中文字符),两个字节才算一个字符
    // 否则一个字节算一个字符
    len = 0;
    for(i=0; str[i]!=''; ++i)
    {
        if( IsDBCSLeadByte(str[i]) )
            ++i;
        ++len;
    }
    
    // 将混合字符转化为宽字符
    wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
    MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
    wstring[len] = L'';
    
    // 用完后记得释放内存
    free(wstring);

    加上前面所讲到的wglUseFontBitmaps函数,即可显示中文字符了。

    void drawCNString(const char* str) {
        int len, i;
        wchar_t* wstring;
        HDC hDC = wglGetCurrentDC();
        GLuint list = glGenLists(1);
    
        // 计算字符的个数
        // 如果是双字节字符的(比如中文字符),两个字节才算一个字符
        // 否则一个字节算一个字符
        len = 0;
        for(i=0; str[i]!=''; ++i)
        {
            if( IsDBCSLeadByte(str[i]) )
                ++i;
            ++len;
        }
    
        // 将混合字符转化为宽字符
        wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
        MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
        wstring[len] = L'';
    
        // 逐个输出字符
        for(i=0; i<len; ++i)
        {
            wglUseFontBitmapsW(hDC, wstring[i], 1, list);
            glCallList(list);
        }
    
        // 回收所有临时资源
        free(wstring);
        glDeleteLists(list, 1);
    }

    注意我用了wglUseFontBitmapsW函数,而不是wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函数的宽字符版本,它认为字符都占两个字节。因为这里使用了MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用wglUseFontBitmapsW。

    void display(void) {
        glClear(GL_COLOR_BUFFER_BIT);
    
        selectFont(48, ANSI_CHARSET, "Comic Sans MS");
        glColor3f(1.0f, 0.0f, 0.0f);
        glRasterPos2f(-0.7f, 0.4f);
        drawString("Hello, World!");
    
        selectFont(48, GB2312_CHARSET, "楷体_GB2312");
        glColor3f(1.0f, 1.0f, 0.0f);
        glRasterPos2f(-0.7f, -0.1f);
        drawCNString("当代的中国汉字");
    
        selectFont(48, DEFAULT_CHARSET, "华文仿宋");
        glColor3f(0.0f, 1.0f, 0.0f);
        glRasterPos2f(-0.7f, -0.6f);
        drawCNString("傳統的中國漢字");
    
        glutSwapBuffers();
    }

    纹理字体
    把文字放到纹理中有很多好处,例如,可以任意修改字符的大小(而不必重新指定字体)。
    对一面飘动的旗帜使用带有文字的纹理,则文字也会随着飘动。这个技术在“三国志”系列游戏中经常用到,比如关羽的部队,旗帜上就飘着个“关”字,张飞的部队,旗帜上就飘着个“张”字,曹操的大营,旗帜上就飘着个“曹”字。三国人物何其多,不可能为每种姓氏都单独制作一面旗帜纹理,如果能够把文字放到纹理上,则可以解决这个问题。(参见后面的例子:绘制一面“曹”字旗)
    如何把文字放到纹理中呢?自然的想法就是:“如果前面所用的显示列表,可以直接往纹理里面绘制,那就好了”。不过,“绘制到纹理”这种技术要涉及的内容可不少,足够我们专门拿一课的篇幅来讲解了。这里我们不是直接绘制到纹理,而是用简单一点的办法:先把汉字绘制出来,成为像素,然后用glCopyTexImage2D把像素复制为纹理。
    glCopyTexImage2D与glTexImage2D的用法是类似的(参见第11课),不过前者是直接把绘制好的像素复制到纹理中,后者是从内存传送数据到纹理中。要使用到的代码大致如下

    // 先把文字绘制好
    glRasterPos2f(XXX, XXX);
    drawCNString("");
    
    // 分配纹理编号
    glGenTextures(1, &texID);
    
    // 指定为当前纹理
    glBindTexture(GL_TEXTURE_2D, texID);
    
    // 把像素作为纹理数据
    // 将屏幕(0, 0) 到 (64, 64)的矩形区域的像素复制到纹理中
    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 64, 64, 0);
    
    // 设置纹理参数
    glTexParameteri(GL_TEXTURE_2D,
        GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,
        GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    然后,我们就可以像使用普通的纹理一样来做了。绘制各种物体时,指定合适的纹理坐标即可。

    有一个细节问题需要特别注意。大家看上面的代码,指定文字显示的位置,写的是glRasterPos2f(XXX, XXX);这里来讲讲如何计算这个显示坐标。
    让我们首先从计算文字的大小谈起。大家知道即使是同一字号的同一个文字,大小也可能是不同的,英文字母尤其如此,有的字体中大写字母O和小写字母l是一样宽的(比如Courier New),有的字体中大写字母O比较宽,而小写字母l比较窄(比如Times New Roman),汉字通常比英文字母要宽。
    为了计算文字的宽度,Windows专门提供了一个函数GetCharABCWidths,它计算一系列连续字符的ABC宽度。所谓ABC宽度,包括了a, b, c三个量,a表示字符左边的空白宽度,b表示字符实际的宽度,c表示字符右边的空白宽度,三个宽度值相加得到整个字符所占宽度。如果只需要得到总的宽度,可以使用GetCharWidth32函数。如果要支持汉字,应该使用宽字符版本,即GetCharABCWidthsW和GetCharWidth32W。在使用前需要用MultiByteToWideChar函数,将通常的字符串转化为宽字符串,就像前面的wglUseFontBitmapsW那样。
    解决了宽度,我们再来看看高度。本来,在指定字体的时候指定大小为s的话,所有的字符高度都为s,只有宽度不同。但是,如果我们使用glRasterPos2i(-1, -1)从最左下角开始显示字符的话,其实是不能得到完整的字符的:(。我们知道英文字母在写的时候可以分上中下三栏,这时绘制出来只有上、中两栏是可见的,下面一栏则不见了,字母g尤其明显。见下图:

    所以,需要把绘制的位置往上移一点,具体来说就是移动下面一栏的高度。这个高度是多少像素呢?这个我也不知道有什么好办法来计算,根据我的经验,移动整个字符高度的八分之一是比较合适的。例如字符大小为24,则移动3个像素。
    还要注意,OpenGL 2.0以前的版本,通常要求纹理的大小必须是2的整数次方,因此我们应该设置字体的高度为2的整数次方,例如16, 32, 64,这样用起来就会比较方便。
    现在让我们整理一下思路。首先要做的是将字符串转化为宽字符的形式,以便使用wglUseFontBitmapsW和GetCharWidth32W函数。然后设置字体大小,接下来计算字体宽度,计算实际绘制的位置。然后产生显示列表,利用显示列表绘制字符,销毁显示列表。最后分配一个纹理编号,把字符像素复制到纹理中。
    呵呵,内容已经不少了,让我们来看看代码。 

    #define FONT_SIZE       64
    #define TEXTURE_SIZE    FONT_SIZE
    
    GLuint drawChar_To_Texture(const char* s) {
        wchar_t w;
        HDC hDC = wglGetCurrentDC();
    
        // 选择字体字号、颜色
        // 不指定字体名字,操作系统提供默认字体
        // 设置颜色为白色
        selectFont(FONT_SIZE, DEFAULT_CHARSET, "");
        glColor3f(1.0f, 1.0f, 1.0f);
    
        // 转化为宽字符
        MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, s, 2, &w, 1);
    
        // 计算绘制的位置
        {
            int width, x, y;
            GetCharWidth32W(hDC, w, w, &width);    // 取得字符的宽度
            x = (TEXTURE_SIZE - width) / 2;
            y = FONT_SIZE / 8;
            glWindowPos2iARB(x, y); // 一个扩展函数
        }
    
        // 绘制字符
        // 绘制前应该将各种可能影响字符颜色的效果关闭
        // 以保证能够绘制出白色的字符
        {
            GLuint list = glGenLists(1);
    
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_LIGHTING);
            glDisable(GL_FOG);
            glDisable(GL_TEXTURE_2D);
    
            wglUseFontBitmaps(hDC, w, 1, list);
            glCallList(list);
            glDeleteLists(list, 1);
        }
    
        // 复制字符像素到纹理
        // 注意纹理的格式
        // 不使用通常的GL_RGBA,而使用GL_LUMINANCE4
        // 因为字符本来只有一种颜色,使用GL_RGBA浪费了存储空间
        // GL_RGBA可能占16位或者32位,而GL_LUMINANCE4只占4位
        {
            GLuint texID;
            glGenTextures(1, &texID);
            glBindTexture(GL_TEXTURE_2D, texID);
            glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE4,
                0, 0, TEXTURE_SIZE, TEXTURE_SIZE, 0);
            glTexParameteri(GL_TEXTURE_2D,
                GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D,
                GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            return texID;
        }
    }

    为了方便,我使用了glWindowPos2iARB这个扩展函数来指定绘制的位置。如果某个系统中OpenGL没有支持这个扩展,则需要使用较多的代码来实现类似的功能。为了方便的调用这个扩展,我使用了GLEE。详细的情形可以看本教程第十四课,最后的那一个例子。GL_ARB_window_pos扩展在OpenGL 1.3版本中已经成为标准的一部分,而几乎所有现在还能用的显卡在正确安装驱动后都至少支持OpenGL 1.4,所以不必担心不支持的问题。
    另外,占用的空间也是需要考虑的问题。通常,我们的纹理都是用GL_RGBA格式,OpenGL会保存纹理中每个像素的红、绿、蓝、alpha四个值,通常,一个像素就需要16或32个二进制位才能保存,也就是2个字节或者4个字节才保存一个像素。我们的字符只有“绘制”和“不绘制”两种状态,因此一个二进制位就足够了,前面用16个或32个,浪费了大量的空间。缓解的办法就是使用GL_LUMINANCE4这种格式,它不单独保存红、绿、蓝颜色,而是把这三种颜色合起来称为“亮度”,纹理中只保存这种亮度,一个像素只用四个二进制位保存亮度,比原来的16个、32个要节省不少。注意这种格式不会保存alpha值,如果要从纹理中取alpha值的话,总是返回1.0。


    应用纹理字体的实例:飘动的旗帜
    (提示:这一段需要一些数学知识)
    有了纹理,只要我们绘制一个正方形,适当的设置纹理坐标,就可以轻松的显示纹理图象了(参见第十一课),因为这里纹理图象实际上就是字符,所以我们也就显示出了字符。并且,随着正方形大小的变化,字符的大小也会随着变化。
    直接贴上纹理,太简单了。现在我们来点挑战性的:画一个飘动的曹操军旗帜。效果如下图,很酷吧?呵呵。

    效果是不错,不过它也不是那么容易完成的,接下来我们一点一点的讲解。 

    为了完成上面的效果,我们需要具备以下的知识:
    1. 用多个四边形(实际上是矩形)连接起来,制作飘动的效果
    2. 使用光照,计算法线向量
    3. 把纹理融合进去

    因为要使用光照,法线向量是不可少的。这里我们通过不共线的三个点来得到三个点所在平面的法线向量。
    从数学的角度看,原理很简单。三个点v1, v2, v3,可以用v2减v1,v3减v1,得到从v1到v2和从v1到v3的向量s1和s2。然后向量s1和s2进行叉乘,得到垂直于s1和s2所在平面的向量,即法线向量。
    为了方便使用,应该把法线向量缩放至单位长度,这个也很简单,计算向量的模,然后向量的每个分量都除以这个模即可。

    #include <math.h>
    
    // 设置法线向量
    // 三个不在同一直线上的点可以确定一个平面
    // 先计算这个平面的法线向量,然后指定到OpenGL
    void setNormal(const GLfloat v1[3],
                   const GLfloat v2[3],
                   const GLfloat v3[3]) {
        // 首先根据三个点坐标,相减计算出两个向量
        const GLfloat s1[] = {
            v2[0]-v1[0], v2[1]-v1[1], v2[2]-v1[2]};
        const GLfloat s2[] = {
            v3[0]-v1[0], v3[1]-v1[1], v3[2]-v1[2]};
    
        // 两个向量叉乘得到法线向量的方向
        GLfloat n[] = {
            s1[1]*s2[2] - s1[2]*s2[1],
            s1[2]*s2[0] - s1[0]*s2[2],
            s1[0]*s2[1] - s1[1]*s2[0]
        };
    
        // 把法线向量缩放至单位长度
        GLfloat abs = sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]);
        n[0] /= abs;
        n[1] /= abs;
        n[2] /= abs;
    
        // 指定到OpenGL
        glNormal3fv(n);
    }

    好的,飘动的旗帜已经做好,现在来看最后的步骤,将纹理贴到旗帜上。
    细心的朋友可能会想到这样一个问题:明明绘制文字的时候使用的是白色,放到纹理中也是白色,那个“曹”字是如何显示为黄色的呢?
    这就要说到纹理的使用方法了。大家在看了第十一课“纹理的使用入门”以后,难免认为纹理就是用一幅图片上的像素颜色来替换原来的颜色。其实这只是纹理最简单的一种用法,它还可以有其它更复杂但是实用的用法。
    这里我们必须提到一个函数:glTexEnv*。从OpenGL 1.0到OpenGL 1.5,每个OpenGL版本都对这个函数进行了修改,如今它的功能已经变的非常强大(但同时也非常复杂,如果要全部讲解,只怕又要花费一整课的篇幅了)。
    最简单的用法就是:

    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);



    它指定纹理的使用方式为“代替”,即用纹理中的颜色代替原来的颜色。
    我们这里使用另一种用法:

    GLfloat color[] = {1.0f, 1.0f, 0.0f, 1.0f};
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);
    glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color);



    其中第二行指定纹理的使用方式为“混合”,它与OpenGL的混合功能类似,但源因子和目标因子是固定的,无法手工指定。最终产生的颜色为:纹理的颜色*常量颜色 + (1.0-纹理颜色)*原来的颜色。常量颜色是由第三行代码指定为黄色。
    因为我们的纹理里面装的是文字,只有黑、白两种颜色。如果纹理中某个位置是黑色,套用上面的公式,发现结果就是原来的颜色,没有变化;如果纹理中某个位置是白色,套用上面的公式,发现结果就是常量颜色。所以,文字的颜色就由常量颜色决定。我们指定常量颜色,也就指定了文字的颜色。

    主要的知识就是这些了,结合前面课程讲过的视图变换(设置观察点)、光照(设置光源、材质),以及动画,飘动的旗帜就算制作完成。
    呵呵,代码已经比较庞大了,限于篇幅,完整的版本这里就不发上来了,不过附件里面有一份源代码flag.c

    缓冲机制
    走出做完旗帜的喜悦后,让我们回到二维文字的问题上来。
    前面说到因为汉字的数目众多,无法在初始化时就为每个汉字都产生一个显示列表。不过,如果每次显示汉字时都重新产生显示列表,效率上也说不过去。一个好的办法就是,把经常使用的汉字的显示列表保存起来,当需要显示汉字时,如果这个汉字的显示列表已经保存,则不再需要重新产生。如果有很多的汉字都需要产生显示列表,占用容量过多,则删除一部分最近没有使用的显示列表,以便释放出一些空间来容纳新的显示列表。
    学过操作系统原理的朋友应该想起来了,没错,这与内存置换的算法是一样的。内存速度快但是容量小,硬盘(虚拟内存)速度慢但是容量大,需要找到一种机制,使性能尽可能的达到最高。这就是内存置换算法。
    常见的内存置换算法有好几种,这里我们选择一种简单的。那就是随机选择一个显示列表并且删除,空出一个位置用来装新的显示列表。
    还要说一下,我们不再直接用显示列表来显示汉字了,改用纹理。因为纹理更加灵活,而且根据实验,纹理比显示列表更快。一个显示列表只能保存一个字符,但是纹理只要足够大,则可以保存很多的字符。假设字符的高度是32,则宽度不超过32,如果纹理是256*256的话,就可以保存8行8列,总共64个汉字。
    我们要做的功能:
    1. 缓冲机制的初始化
    2. 缓冲机制的退出
    3. 根据一个文字字符,返回对应的纹理坐标。如果字符本身不在纹理中,则应该先把字符加入到纹理中(如果纹理已经装不下了,则先删除一个),然后返回纹理坐标。
    要改进缓冲机制的性能,则应该使用更高效的置换算法,不过这个已经远超出OpenGL的范围了。大家如果有空也可以看看linux源码什么的,应该会找到好的置换算法。
    即使我们使用最简单的置换算法,完整的代码仍然有将近200行,其实这些都是算法基本功了,跟OpenGL关系并不太大。仍然是由于篇幅限制,仅在附件中给出,就不贴在这里了。文件名为ctbuf.h和ctbuf.c,在使用的时候把这两个文件都加入到工程中,并调用ctbuf.h中声明的函数即可。
    这里我们仅仅给出调用部分的代码。

    #include "ctbuf.h"
    
    void display(void) {
        static int isFirstCall = 1;
    
        if( isFirstCall ) {
            isFirstCall = 0;
            ctbuf_init(32, 256, "黑体");
        }
    
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
        glEnable(GL_TEXTURE_2D);
        glPushMatrix();
        glTranslatef(-1.0f, 0.0f, 0.0f);
        ctbuf_drawString("美好明天就要到来", 0.1f, 0.15f);
        glTranslatef(0.0f, -0.15f, 0.0f);
        ctbuf_drawString("Best is yet to come", 0.1f, 0.15f);
        glPopMatrix();
    
        glutSwapBuffers();
    }

    注意这里我们是用纹理来实现字符显示的,因此文字的大小会随着窗口大小而变化。最初的Hello, World程序就不会有这样的效果,因为它的字体硬性的规定了大小,不如纹理来得灵活。 

    显示大段的文字


    有了缓冲机制,显示文字的速度会比没有缓冲时快很多,这样我们也可以考虑显示大段的文字了。
    基本上,前面的ctbuf_drawString函数已经可以快速的显示一个较长的字符串,但是它有两个缺点。
    第一个缺点是不会换行,一直横向显示到底。
    第二个缺点是即使字符在屏幕以外,也会尝试在缓冲中查找这个字符,如果没找到,还会重新生成这个字符。

    让我们先来看看第一个问题,换行。所谓换行其实就是把光标移动到下一行的开头,如果知道每一行开头的位置的话,只需要很短的代码就可以实现。
    不过,OpenGL显示文字的时候并不会保存每一行开头的位置,所以这个需要我们自己动手来做。
    第二个问题是关于性能的,如果字符本身不会显示出来,那么为它产生显示列表和纹理就是一种浪费,如果为了容纳它的显示列表或者纹理,而把缓冲区中其它有用的字符的显示列表或者纹理给删除了,那就更加得不偿失。
    所以,判断字符是否会显示也是很重要的。像我们的浏览器,如果显示一个巨大的网页,其实它也只绘制最必要的部分。
    为了解决上面两个问题,我们再单独的编写一个模块。初始化的时候指定显示区域的大小、每行多少个字符、每列多少个字符,在模块内部判断是否需要换行,以及判断每个文字是否真的需要显示。

    呃,小小的感慨一下,为什么每当我做好一份代码,就发现它实在太长,长到我不想贴出来呢?唉……
    先看看图:

    注意观察就可以发现,歌词分为多行,只有必要的行才会显示,不会从头到尾的显示出来。
    代码中主要是算法和C语言基本功,跟OpenGL关系并不大。还是照旧,把主要的代码放到附件里,文件名为textarea.h和textarea.c,使用时要与前面的ctbuf.h和ctbuf.c一起使用。
    这里仅给出调用部分的代码。 

    const char* g_string =
        "《合金装备》(Metal Gear Solid)结尾曲歌词
    "
        // 歌词很多很长
        "因为。。。。。。。。 
    "
        "美好即将到来
    ";
    
    textarea_t* p_textarea = NULL;
    
    void display(void) {
        static int isFirstCall = 1;
    
        if( isFirstCall ) {
            isFirstCall = 0;
            ctbuf_init(24, 256, "隶书");
            p_textarea = ta_create(-0.7f, -0.5f, 0.7f, 0.5f,
                20, 10, g_string);
            glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
        }
    
        glClear(GL_COLOR_BUFFER_BIT);
    
        // 显示歌词文字
        glEnable(GL_TEXTURE_2D);
        ta_display(p_textarea);
    
        // 用半透明的效果显示一个方框
        // 这个框是实际需要显示的范围
        glEnable(GL_BLEND);
        glDisable(GL_TEXTURE_2D);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
        glRectf(-0.7f, -0.5f, 0.7f, 0.5f);
        glDisable(GL_BLEND);
    
        // 显示一些帮助信息
        glEnable(GL_TEXTURE_2D);
        glPushMatrix();
        glTranslatef(-1.0f, 0.9f, 0.0f);
        ctbuf_drawString("歌词显示程序", 0.1f, 0.1f);
        glTranslatef(0.0f, -0.1f, 0.0f);
        ctbuf_drawString("按W/S键实现上、下翻页", 0.1f, 0.1f);
        glTranslatef(0.0f, -0.1f, 0.0f);
        ctbuf_drawString("按ESC退出", 0.1f, 0.1f);
        glPopMatrix();
    
        glutSwapBuffers();
    }

    轮廓字体
    其实上面我们所讲那么多,只讲了一类字体,即像素字体,此外还有轮廓字体。所以,这个看似已经很长的课程,其实只讲了“显示文字”这个课题的一半。估计大家已经看不下去了,其实我也写不下去了。好长……
    那么,本课就到这里吧。有种虎头蛇尾的感觉:(
    小结

    本课的内容不可谓不多。列表如下:
    1. 以Hello, World开始,说明英文字符(ASCII字符)是如何绘制的。
    2. 给出了一个设置字体的函数selectFont。
    3. 讲了如何显示中文字符。
    4. 讲了如何把字符保存到纹理中。
    5. 给出了一个大的例子,绘制一面“曹”字旗。(附件flag.c)
    6. 讲解了缓冲机制,其实跟内存的置换算法原理是一样的。我们给出了一个最简单的缓冲实现,采用随机的置换算法。(做成了模块,附件ctbuf.h,ctbuf.c,调用的例子在本课正文中可以找到)
    7. 通过缓冲机制,实现显示大段的文字。主要是注意换行的处理,还有就是只显示必要的行。(做成了模块,附件textarea.h,textarea.c,调用的例子在本课正文中可以找到)
    最后两个模块虽然是以附件形式给出的,但是原理我想我已经说清楚了,并且这些内容跟OpenGL关系并不大,主要还是相关专业的知识,或者C语言基本功。主要是让大家弄清楚原理,附件代码只是作为参考用。
    说说我的感受:显示文字很简单,显示文字很复杂。除了最基本的显示列表、纹理等OpenGL常识外,更多的会涉及到数学、数据结构与算法、操作系统等各个领域。一个大型的程序通常都要实现一些文字特殊效果,仅仅是调用几个显示列表当然是不行的,需要大量的相关知识来支撑。
    本课的门槛突然提高,搞得我都不知道这还算不算是“入门教程”了,希望各位不要退缩哦。祝大家愉快。 


  • 相关阅读:
    【转】Android开发——MediaProvider源码分析(2)
    关于ActivityGroup使用过程中遇到的一点问题
    HttpWebRequest详解
    关于Assembly.CreateInstance()与Activator.CreateInstance()方法的区别
    你会在C#的类库中添加web service引用吗?
    ASP.NET跳转网页的三种方法的比较
    .net发送HTTP POST包
    依赖注入
    微软ASP.NET MVC Beta版本发布
    随笔~
  • 原文地址:https://www.cnblogs.com/Anita9002/p/4422690.html
Copyright © 2020-2023  润新知