回 帖 发 新 帖 刷新版面

主题:[原创]OpenGL入门学习——第十六课

现在即将放出的是第十六课的内容。

首先还是以前课程的连接:
[url=http://www.programfan.com/club/showbbs.asp?id=184355]第一课,编写第一个OpenGL程序[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=184525]第二课,绘制几何图形[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=184769]第三课,绘制几何图形的一些细节问题[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=185032]第四课,颜色的选择[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=196017]第五课,三维的空间变换[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=196231]第六课,动画的制作[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=218828]第七课,使用光照来表现立体感[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=219518]第八课,使用显示列表[/url]
[url=http://www.programfan.com/club/showbbs.asp?id=224877]第九课,使用混合来实现半透明效果[/url]
[url=http://www.programfan.com/club/post-227694.html]第十课,BMP文件与像素操作[/url]
[url=http://www.programfan.com/club/post-244703.html]第十一课,纹理的使用入门[/url]
[url=http://bbs.pfan.cn/post-252901.html]第十二课,OpenGL片断测试[/url]
[url=http://bbs.pfan.cn/post-275218.html]第十三课,OpenGL是一个状态机[/url]
[url=http://bbs.pfan.cn/post-275219.html]第十四课,OpenGL版本和OpenGL扩展[/url]
[url=http://bbs.pfan.cn/post-275223.html]第十五课,从“绘制一个立方体”来看OpenGL的进化过程[/url]
第十六课,在Windows系统中显示文字  ——→  [color=0000FF]本次课程的内容[/color]

内容超多的一课!不过我想精彩的程度也一定不会让大家失望。大家不妨先浏览一下课程里的图片:)。

[color=FF0000]2008-06-10 修改了附件[/color]
增加了两个文件,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可以[url=http://www.opengl.org/sdk/libs/GLee/GLee5_21.zip]从网上下载[/url],因此这里并没有放到附件中。在编译的时候应该将这两个文件和其它代码文件一起编译。

=====================未完,请勿跟帖=====================

回复列表 (共106个回复)

沙发

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

=====================未完,请勿跟帖=====================

板凳

[color=FF0000]OpenGL版的“Hello, World!”[/color]
写完了本课,我的感受是:显示文字很简单,显示文字很复杂。看似简单的功能,背后却隐藏了深不可测的玄机。
呵呵,别一开始就被吓住了,让我们先从“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>。
现在让我们来看具体的代码:
[code=c]#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!='\0'; ++str)
        glCallList(lists + *str);
}[/code]

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

    glColor3f(1.0f, 0.0f, 0.0f);
    glRasterPos2f(0.0f, 0.0f);
    drawString("Hello, World!");

    glutSwapBuffers();
}[/code]

效果如图:
[img]http://blog.programfan.com/upfile/200805/20080505132619.gif[/img]

=====================未完,请勿跟帖=====================

3 楼

[color=FF0000]指定字体[/color]
在产生显示列表前,Windows允许选择字体。
我做了一个selectFont函数来实现它,大家可以看看代码。
[code=c]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();
}[/code]
最主要的部分就在于那个参数超多的CreateFont函数,学过Windows GDI的朋友应该不会陌生。没有学过GDI的朋友,有兴趣的话可以自己翻翻MSDN文档。这里我并不准备仔细讲这些参数了,下面的内容还多着呢:(
如果需要在自己的程序中选择字体的话,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。
效果如图:
[img]http://blog.programfan.com/upfile/200805/20080505132624.gif[/img]

=====================未完,请勿跟帖=====================

4 楼

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

// 用完后记得释放内存
free(wstring);[/code]

加上前面所讲到的wglUseFontBitmaps函数,即可显示中文字符了。
[code=c]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]!='\0'; ++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'\0';

    // 逐个输出字符
    for(i=0; i<len; ++i)
    {
        wglUseFontBitmapsW(hDC, wstring[i], 1, list);
        glCallList(list);
    }

    // 回收所有临时资源
    free(wstring);
    glDeleteLists(list, 1);
}[/code]

注意我用了wglUseFontBitmapsW函数,而不是wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函数的宽字符版本,它认为字符都占两个字节。因为这里使用了MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用wglUseFontBitmapsW。
[code=c]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();
}[/code]
效果如图:
[img]http://blog.programfan.com/upfile/200805/20080505132632.gif[/img]

=====================未完,请勿跟帖=====================

5 楼

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


有一个细节问题需要特别注意。大家看上面的代码,指定文字显示的位置,写的是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尤其明显。见下图:
[img]http://blog.programfan.com/upfile/200805/20080505132638.gif[/img]

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

=====================未完,请勿跟帖=====================

6 楼

[code=c]#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;
    }
}[/code]

=====================未完,请勿跟帖=====================

7 楼

为了方便,我使用了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。

=====================未完,请勿跟帖=====================

8 楼

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

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

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

=====================未完,请勿跟帖=====================

9 楼

先不管那个大大的“曹”字,我们来看怎样制作出飘动的形状的。
首先让我们回忆一下,如何画一个圆。首先画一个正六边形,然后在六边形的基础上修改,每一边变化为两边,成为正十二边形,继续变化,得到正二十四边形、正四十八边形,越往后进行,形状就越接近圆了。
飘动的形状也是这样制作出来的。最开始的时候,整个旗帜是一个矩形,每次变化时,从水平方向上将每个矩形分成两段,下面的几幅图片表示了变化的过程。四幅图片依次是:分为两段、分为四段、分为八段、分为十六段时的效果。

[img]http://blog.pfan.cn/upfile/200805/20080505132648.jpg[/img][img]http://blog.pfan.cn/upfile/200805/20080505132654.jpg[/img]
[img]http://blog.pfan.cn/upfile/200805/20080505132659.jpg[/img][img]http://blog.pfan.cn/upfile/200805/2008050513274.jpg[/img]

旗帜摆动时,我们简单的认为:旗帜的每一个点的x坐标和y坐标都不变,只有z坐标发生变化。而且z坐标是呈波浪形状的变化,这种变化规律可以用正弦函数来表示。
对一个固定的点,其z坐标可以表示为z = sin(t * c1 + c2) * c3 + c4,其中c1, c2, c3, c4都是常数。从纯数学的角度分析,c4可以表示偏移值,这里只要简单的设置为零,c3表示了摆动的幅度,c2可以设置为一个跟点的x坐标相关的量,c1表示了摆动的速度。
让我们来看看代码吧。首先需要定义较多的常量:(
[quote]#define MIN_X       (-0.5f)
#define MAX_X       (0.5f)
#define MIN_Y       (-0.5f)
#define MAX_Y       (0.5f)
#define SEGS        ((int)((MAX_X - MIN_X) * (512/2)))
#define RANGE       (0.05f)
#define CIRCLES     (2.0f)
#define SPEED       (5.0f)
#define PI          (3.1415926f)[/quote]

前面四个量表示了旗帜的大小范围。
SEGS表示分段数,分段越多则显示越细致,这里我把它定义成了与旗帜宽度相关的量。
RANGE表示摆动幅度,CIRCLES表示一面旗帜会出现多少个波峰,SPEED表示摆动速度。这三个量分别与公式z = sin(t * c1 + c2) * c3 + c4中的c3, c2, c1成正比。
最后还要定义一个常量PI,表示圆周率。
有了这些常量,我们就可以编写具体的代码了:

[quote]#include <math.h>

// theta是一个随时间变化的量
GLfloat theta = 0.0f;

// 绘制一面旗帜
void draw(void) {
    int i;
    // 每绘制一段,坐标x应该增加的量
    const GLfloat x_inc = (MAX_X - MIN_X) / SEGS;
    // 每绘制一段,纹理坐标s应该增加的量
    const GLfloat t_inc = 1.0f / SEGS;
    // 每绘制一段,常数theta应该增加的量
    const GLfloat theta_inc = 2 * PI * CIRCLES / SEGS;

    // 用GL_QUAD_STRIP来绘制相连的四边形
    glBegin(GL_QUAD_STRIP);
    for(i=0; i<=SEGS; ++i) {
        // 按照z = sin(t * c1 + c2) * c3 + c4的公式计算z坐标
        const GLfloat z = RANGE * sin(i*theta_inc + theta);

        // 一段只需要指定两个点
        // 第三个点其实是下一段的第一个点
        // 之所以使用三个点,是为了构成一个平面
        // 便于计算法线向量
        const GLfloat
            v1[] = {i*x_inc + MIN_X, MAX_Y, z},
            v2[] = {i*x_inc + MIN_X, MIN_Y, z},
            v3[] = {
                (i+1)*x_inc + MIN_X,
                MAX_Y,
                RANGE * sin((i+1)*theta_inc + theta)};

        // 调用一个函数来计算法线向量
        setNormal(v1, v2, v3);

        // 设置合适的纹理坐标和顶点坐标
        glTexCoord2f(i*t_inc, 1.0f);
        glVertex3fv(v1);
        glTexCoord2f(i*t_inc, 0.0f);
        glVertex3fv(v2);
    }
    glEnd();
}

// 系统空闲时调用
// 增加theta的值,然后重新绘制
void idle(void) {
    theta += (SPEED * PI / 180.0f);
    glutPostRedisplay();
}[/quote]

10 楼

呃,刚才被叫去开会……
接着发。

我来回复

您尚未登录,请登录后再回复。点此登录或注册