回 帖 发 新 帖 刷新版面

主题:[原创]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]
第十二课,OpenGL片断测试  ——→  [color=0000FF]本次课程的内容[/color]

片断测试其实就是测试每一个像素,只有通过测试的像素才会被绘制,没有通过测试的像素则不进行绘制。OpenGL提供了多种测试操作,利用这些操作可以实现一些特殊的效果。
我们在前面的课程中,曾经提到了“深度测试”的概念,它在绘制三维场景的时候特别有用。在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。
如果使用了深度测试,则情况就会有所不同:每当一个像素被绘制,OpenGL就记录这个像素的“深度”(深度可以理解为:该像素距离观察者的距离。深度值越大,表示距离越远),如果有新的像素即将覆盖原来的像素时,深度测试会检查新的深度是否会比原来的深度值小。如果是,则覆盖像素,绘制成功;如果不是,则不会覆盖原来的像素,绘制被取消。这样一来,即使我们先绘制比较近的物体,再绘制比较远的物体,则远的物体也不会覆盖近的物体了。
实际上,只要存在深度缓冲区,无论是否启用深度测试,OpenGL在像素被绘制时都会尝试将深度数据写入到缓冲区内,除非调用了glDepthMask(GL_FALSE)来禁止写入。这些深度数据除了用于常规的测试外,还可以有一些有趣的用途,比如绘制阴影等等。

除了深度测试,OpenGL还提供了剪裁测试、Alpha测试和模板测试。

[color=0000FF]因为论坛开始支持附件,现在把程序源代码和所使用的图片一起打包上传[/color]

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

回复列表 (共19个回复)

11 楼

不好意思,忘了不能跟帖

12 楼

真的搞不懂,如何创建顶点缓冲和索引缓冲呢?顶点的骨骼权重如何设置?

13 楼

[quote]不好意思,忘了不能跟帖[/quote]
呵呵,本课已经发完,可以跟贴了呀。

[quote]真的搞不懂,如何创建顶点缓冲和索引缓冲呢?顶点的骨骼权重如何设置?[/quote]
顶点缓冲和索引缓冲是OpenGL 1.5版本所提供的功能,因此首先检查OpenGL版本是否达到1.5。因为Windows仅直接支持OpenGL 1.1的函数,更高版本的函数应该使用wglGetProcAddress函数来获得这些函数的指针,然后利用函数指针进行间接调用。而且这些函数所需要使用的常量在一个叫做glext.h的头文件中定义,可在google上搜索下载该文件的最新版本。
Vertex Buffer Object(顶点缓冲对象)所涉及的函数:
glGenBuffers:分配缓冲对象编号
glBindBuffer:绑定缓冲。可以绑定顶点缓冲和索引缓冲。
glBufferData,glBufferSubData:修改缓冲中的全部数据或部分数据。第一次修改时应该使用glBufferData,以后可以选择使用glBufferData或glBufferSubData。
glMapBuffer,glUnmapBuffer:glMapBuffer锁定缓冲数据并把缓冲数据映射到内存,然后可以用一个指针进行灵活的访问和修改,修改完成后利用glUnmapBuffer确认修改并解除锁定。

骨骼权重这个概念我并不怎么清楚,这个也就不能回答了。

14 楼

八点半上班,不好意思。
中午再给个顶点缓冲/索引缓冲的例子吧。

15 楼

顶点缓冲/索引缓冲使用示例。
注意:该程序使用C语言编写(不是C++)。使用了两个工具包,GLUT和GLEE。其中:GLUT的安装方法在本课程的第一课里面有描述。GLEE实际上就是两个文件glee.h和glee.c,从网上下载这两个文件的最新版本并放到工程中,和下面的代码一起编译。

代码过长,分开发送。

[code=c]#define WindowWidth  512
#define WindowHeight 512
#define WindowTitle  "OpenGL -- Vertex Buffer Objects 测试"

#include "GLee.h"
#include <GL/glut.h>
#include <stdio.h>

static GLfloat gf_RotateAngle = 0.0f;
static int     gi_Rotating = 1;

void display(void)
{
#define length_half (1.0f)
    // 混合数组,用六个值表示一个顶点(前三项为颜色,后三项为顶点坐标),共8个顶点
    static GLfloat vertex_list[6*8] =
    {
        0.0f, 0.0f, 0.0f, -length_half, -length_half, -length_half,
        0.0f, 0.0f, 1.0f, -length_half, -length_half,  length_half,
        0.0f, 1.0f, 0.0f, -length_half,  length_half, -length_half,
        0.0f, 1.0f, 1.0f, -length_half,  length_half,  length_half,
        1.0f, 0.0f, 0.0f,  length_half, -length_half, -length_half,
        1.0f, 0.0f, 1.0f,  length_half, -length_half,  length_half,
        1.0f, 1.0f, 0.0f,  length_half,  length_half, -length_half,
        1.0f, 1.0f, 1.0f,  length_half,  length_half,  length_half
    };
    // 索引数组,每四个顶点表示一个平面,共六个平面
    static GLuint index_list[4*6] =
    {
        0, 1, 3, 2,
        4, 5, 7, 6,
        0, 2, 6, 4,
        1, 3, 7, 5,
        0, 1, 5, 4,
        2, 3, 7, 6
    };
    // 标记本函数是否为第一次调用,如果是,可进行初始化
    static int isFirstCall = 1;
#undef length_half
[/code]

16 楼

接楼上。

[code=c]    // 清除屏幕
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);

    // 设置视角
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60, 1, 1, 10);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(3, 3, 3, 0, 0, 0, 0, 1, 0);

    // 旋转
    glRotatef(gf_RotateAngle, 0.0f, 1.0f, 1.0f);

    // 初始化GLEE,GLEE会自动读取动态连接库中的1.1以上版本OpenGL函数(如果有的话)
    GLeeInit();

    // 根据OpenGL所支持VBO的情况,有三种方式执行渲染
    if( _GLEE_VERSION_1_5 ) // 支持OpenGL 1.5,使用标准的VBO函数
    {
        if( isFirstCall )
        {
            GLuint iVertexBuffer = 0;
            GLuint iIndexBuffer  = 0;

            // 设置顶点缓冲
            glGenBuffers(1, &iVertexBuffer);
            glBindBuffer(GL_ARRAY_BUFFER, iVertexBuffer);
            glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_list),
                vertex_list, GL_STATIC_DRAW);

            // 设置索引缓冲
            glGenBuffers(1, &iIndexBuffer);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iIndexBuffer);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index_list),
                index_list, GL_STATIC_DRAW);

            // 其它设置与顶点数组的设置方式类似,只有在设置指针时不同
            // 不要直接使用指针,而使用偏移地址
            // vertex_list中的数据已经被保存到iVertexBuffer所对应的缓冲对象中,因此使用零作为偏移地址
            // 如果缓冲对象中有很多杂乱的数据,则通过指定不同的偏移地址即可选择不同数据,不需要重新绑定
            glEnableClientState(GL_VERTEX_ARRAY);
            glEnableClientState(GL_COLOR_ARRAY);
            glInterleavedArrays(GL_C3F_V3F, 0, 0);
        }
        // 不要直接使用指针,而使用偏移地址
        glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, 0);
    }
    else if( _GLEE_ARB_vertex_buffer_object ) // 不支持OpenGL 1.5,但以ARB扩展的形式支持VBO
    {
        // 与标准形式的VBO几乎相同
        // 只是函数名有无ARB后缀,常量名有无_ARB后缀这一点区别
        if( isFirstCall )
        {
            GLuint iVertexBuffer = 0;
            GLuint iIndexBuffer  = 0;

            glGenBuffersARB(1, &iVertexBuffer);
            glBindBufferARB(GL_ARRAY_BUFFER_ARB, iVertexBuffer);
            glBufferDataARB(GL_ARRAY_BUFFER_ARB, sizeof(vertex_list),
                vertex_list, GL_STATIC_DRAW_ARB);

            glGenBuffersARB(1, &iIndexBuffer);
            glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, iIndexBuffer);
            glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, sizeof(index_list),
                index_list, GL_STATIC_DRAW_ARB);

            glEnableClientState(GL_VERTEX_ARRAY);
            glEnableClientState(GL_COLOR_ARRAY);
            glInterleavedArrays(GL_C3F_V3F, 0, 0);
        }
        glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, 0);
    }
    else // 不支持VBO,使用Vertex Array代替
    {
        if( isFirstCall )
        {
            printf("your system does not support the VBO.\n");
            glEnableClientState(GL_VERTEX_ARRAY);
            glEnableClientState(GL_COLOR_ARRAY);
            glInterleavedArrays(GL_C3F_V3F, 0, vertex_list);
        }
        glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);
    }

    // 交换缓冲
    glutSwapBuffers();

    isFirstCall = 0;
}

void idle(void)
{
    if( gi_Rotating )
    {
        gf_RotateAngle += 0.1f;
        if( gf_RotateAngle >= 360.0f )
            gf_RotateAngle = 0.0f;
    }
    display();
}

void keyboard(unsigned char c, int x, int y)
{
    gi_Rotating = !gi_Rotating;
}

int main(int argc, char* argv[])
{
    // GLUT初始化
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(WindowWidth, WindowHeight);
    glutCreateWindow(WindowTitle);
    glutDisplayFunc(&display);
    glutIdleFunc(&idle);
    glutKeyboardFunc(&keyboard);

    // 开始显示
    glutMainLoop();

    return 0;
}[/code]

17 楼

顶点缓冲对象(Vertex Buffer Object, VBO)的使用步骤如下:
1、用glGenBuffers分配缓冲对象编号,该函数的使用方法与分配纹理对象编号类似。
2、用glBindBuffer绑定缓冲对象,可以用GL_ARRAY_BUFFER来绑定到顶点缓冲,也可以用GL_ELEMENT_ARRAY_BUFFER来绑定到索引缓冲。
3、用glBufferData指定缓冲中的数据,需要分别指定顶点缓冲的数据和索引缓冲的数据。注意该函数的最后一个参数,指明了该数据的用途,这个用途将帮助OpenGL决定如何进行优化。其中GL_STATIC_DRAW表示数据一旦确定,几乎不会更改,而且数据是用于绘制的。

成功使用以上三个函数后,很多本来接受指针的OpenGL函数在用法上就会发生变化。例如glColor3fv函数,本来接受一个指针,该指针指向三个连续的GLfloat类型的值,用于确定颜色。但因为现在绑定了GL_ARRAY_BUFFER,所以该函数接收一个偏移值offset,表示从已经绑定的缓冲中第offset个字节开始,取三个GLfloat类型的值,用于确定颜色。(注意:虽然偏移值是一个整数,但是你还是需要将它强制转换为glColor3fv所接受的指针类型,以确保编译能够顺利完成。)

在OpenGL 1.1版本中,提供了“顶点数组”的功能,可以把很多顶点数据放到数组中,然后通过调用很少的函数就完成绘制,而不必像OpenGL 1.0那样使用glBegin, glEnd以及大量的glColor*, glNormal*, glTexCoord*, glVertex*等函数。这些功能都是通过指针完成的。因此在绑定了顶点缓冲后,也可以用缓冲中的数据和指定偏移值的方式来代替原来的数组。数组是保存在内存中的,而缓冲数据则有可能是直接保存在显卡上,因此有望得到性能的优化。

本程序绘制一个旋转的立方体,按任意键可以开启/关闭旋转。
利用一个数组保存立方体中八个顶点的颜色和坐标,利用一个数组表示立方体六个面中每一面所包含的顶点的索引,然后利用顶点缓冲和顶点数组联合进行绘制。可以看到,除了第一次初始化外,以后只需要调用glDrawElements就可以完成绘制立方体所需要的所有动作。
程序有三种实现。当OpenGL版本为1.5或以上时,直接使用顶点缓冲对象;当OpenGL版本不足1.5,但支持ARB扩展形式的顶点缓冲对象时(此时也需要OpenGL 1.4版本),使用该形式的顶点缓冲对象;如果以上两者都不满足,则只好放弃使用顶点缓冲对象,而直接使用顶点数组。最后一种方式在性能上将会略低于前两种。
我测试用的是Intel的集成显卡,支持OpenGL 1.4以及ARB扩展形式的顶点缓冲对象。因此后两种方式我都测试并正确运行。至于第一种方式,就拜托有独立显卡(可能至少需要Geforce4 MX 440级别的显卡并安装最新驱动)的朋友去测试了。

18 楼

十分感谢eastcowboy的文章,对我们这些新手来说讲得深入浅出。
另外,eastcowboy能不能推荐一到两本自己认为比较好的进阶教程啊,谢谢!

19 楼

收益匪浅,非常感谢,

我来回复

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