OpenGL 入门

转载自 http://www.cppblog.com/doing5552/archive/2009/01/08/71532.htmlhtml

 

提及编程做图,大概还有不少人想起TC的#include <graphics.h>吧?linux

可是各位是否想过,那些画面绚丽的PC游戏是如何编写出来的?就靠TC那可怜的640*480分辨率、16色来作吗?显然是不行的。程序员

本帖的目的是让你们放弃TC的老旧图形接口,让你们接触一些新事物。算法

OpenGL做为当前主流的图形API之一,它在一些场合具备比DirectX更优越的特性。数据库

一、与C语言紧密结合。编程

OpenGL命令最初就是用C语言函数来进行描述的,对于学习过C语言的人来说,OpenGL是容易理解和学习的。若是你曾经接触过TC的graphics.h,你会发现,使用OpenGL做图甚至比TC更加简单。小程序

二、强大的可移植性。windows

微软的Direct3D虽然也是十分优秀的图形API,但它只用于Windows系统(如今还要加上一个XBOX游戏机)。而OpenGL不只用于 Windows,还能够用于Unix/Linux等其它系统,它甚至在大型计算机、各类专业计算机(如:医疗用显示设备)上都有应用。而且,OpenGL 的基本命令都作到了硬件无关,甚至是平台无关。数组

三、高性能的图形渲染。浏览器

OpenGL是一个工业标准,它的技术紧跟时代,现今各个显卡厂家无一不对OpenGL提供强力支持,激烈的竞争中使得OpenGL性能一直领先。

总之,OpenGL是一个很NB的图形软件接口。至于究竟有多NB,去看看DOOM3和QUAKE4等专业游戏就知道了。

OpenGL官方网站(英文)

http://www.opengl.org

下面将对Windows下的OpenGL编程进行简单介绍。

学习OpenGL前的准备工做

第一步,选择一个编译环境

如今Windows系统的主流编译环境有Visual Studio,Broland C++ Builder,Dev-C++等,它们都是支持OpenGL的。但这里咱们选择Visual Studio 2005做为学习OpenGL的环境。

第二步,安装GLUT工具包

GLUT不是OpenGL所必须的,但它会给咱们的学习带来必定的方便,推荐安装。

Windows环境下的GLUT下载地址:(大小约为150k)

http://www.opengl.org/resources/libraries/glut/glutdlls37beta.zip

没法从以上地址下载的话请使用下面的链接:

http://upload.programfan.com/upfile/200607311626279.zip

蓝奏云

https://www.lanzous.com/i5x506j

Windows环境下安装GLUT的步骤:

一、将下载的压缩包解开,将获得5个文件

二、在“个人电脑”中搜索“gl.h”,并找到其所在文件夹(若是是VisualStudio2005,则应该是其安装目录下面的“VC\PlatformSDK\include\gl文件夹”)。把解压获得的glut.h放到这个文件夹。

三、把解压获得的glut.lib和glut32.lib放到静态函数库所在文件夹(若是是VisualStudio2005,则应该是其安装目录下面的“VC\lib”文件夹)。

四、把解压获得的glut.dll和glut32.dll放到操做系统目录下面的system32文件夹内。(典型的位置为:C:\Windows\System32,64位系统应放在SysWOW64文件夹)

第三步,创建一个OpenGL工程

这里以VisualStudio2005为例。

选择File->New->Project,而后选择Win32 Console Application,选择一个名字,而后按OK。

在谈出的对话框左边点Application Settings,找到Empty project并勾上,选择Finish。

而后向该工程添加一个代码文件,取名为“OpenGL.c”,注意用.c来做为文件结尾。

搞定了,就跟平时的工程没什么两样的。

 

第一个OpenGL程序

一个简单的OpenGL程序以下:(注意,若是须要编译并运行,须要正确安装GLUT,安装方法如上所述)

#include <GL/glut.h>

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glRectf(-0.5f, -0.5f, 0.5f, 0.5f);

     glFlush();

}

int main(int argc, char *argv[])

{

     glutInit(&argc, argv);

     glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);

     glutInitWindowPosition(100, 100);

     glutInitWindowSize(400, 400);

     glutCreateWindow("第一个OpenGL程序");

     glutDisplayFunc(&myDisplay);

     glutMainLoop();

     return 0;

}

该程序的做用是在一个黑色的窗口中央画一个白色的矩形。下面对各行语句进行说明。

首先,须要包含头文件#include <GL/glut.h>,这是GLUT的头文件。

原本OpenGL程序通常还要包含<GL/gl.h>和<GL/glu.h>,但GLUT的头文件中已经自动将这两个文件包含了,没必要再次包含。

而后看main函数。

int main(int argc, char *argv[]),这个是带命令行参数的main函数,各位应该见过吧?没见过的同志们请多翻翻书,等弄明白了再往下看。

注意main函数中的各语句,除了最后的return以外,其他所有以glut开头。这种以glut开头的函数都是GLUT工具包所提供的函数,下面对用到的几个函数进行介绍。

一、glutInit,对GLUT进行初始化,这个函数必须在其它的GLUT使用以前调用一次。其格式比较死板,通常照抄这句glutInit(&argc, argv)就能够了。

二、 glutInitDisplayMode,设置显示方式,其中GLUT_RGB表示使用RGB颜色,与之对应的还有GLUT_INDEX(表示使用索引颜色)。GLUT_SINGLE表示使用单缓冲,与之对应的还有GLUT_DOUBLE(使用双缓冲)。更多信息,请本身Google。固然之后的教程也会有一些讲解。

三、glutInitWindowPosition,这个简单,设置窗口在屏幕中的位置。

四、glutInitWindowSize,这个也简单,设置窗口的大小。

五、glutCreateWindow,根据前面设置的信息建立窗口。参数将被做为窗口的标题。注意:窗口被建立后,并不当即显示到屏幕上。须要调用glutMainLoop才能看到窗口。

六、glutDisplayFunc,设置一个函数,当须要进行画图时,这个函数就会被调用。(这个说法不够准确,但准确的说法可能初学者不太好理解,暂时这样说吧)。

七、glutMainLoop,进行一个消息循环。(这个可能初学者也不太明白,如今只须要知道这个函数能够显示窗口,而且等待窗口关闭后才会返回,这就足够了。)

在glutDisplayFunc函数中,咱们设置了“当须要画图时,请调用myDisplay函数”。因而myDisplay函数就用来画图。观察myDisplay中的三个函数调用,发现它们都以gl开头。这种以gl开头的函数都是OpenGL的标准函数,下面对用到的函数进行介绍。

一、glClear,清除。GL_COLOR_BUFFER_BIT表示清除颜色,glClear函数还能够清除其它的东西,但这里不做介绍。

二、glRectf,画一个矩形。四个参数分别表示了位于对角线上的两个点的横、纵坐标。

三、glFlush,保证前面的OpenGL命令当即执行(而不是让它们在缓冲区中等待)。其做用跟fflush(stdout)相似。

OpenGL入门学习[二]

本次课程所要讲的是绘制简单的几何图形,在实际绘制以前,让咱们先熟悉一些概念。

1、点、直线和多边形
咱们知道数学(具体的说,是几何学)中有点、直线和多边形的概念,但这些概念在计算机中会有所不一样。
数学上的点,只有位置,没有大小。但在计算机中,不管计算精度如何提升,始终不能表示一个无穷小的点。另外一方面,不管图形输出设备(例如,显示器)如何精确,始终不能输出一个无穷小的点。通常状况下,OpenGL中的点将被画成单个的像素(像素的概念,请本身搜索之~),虽然它可能足够小,但并不会是无穷小。同一像素上,OpenGL能够绘制许多坐标只有稍微不一样的点,但该像素的具体颜色将取决于OpenGL的实现。固然,过分的注意细节就是钻牛角尖,咱们大可没必要花费过多的精力去研究“多个点如何画到同一像素上”。
一样的,数学上的直线没有宽度,但OpenGL的直线则是有宽度的。同时,OpenGL的直线必须是有限长度,而不是像数学概念那样是无限的。能够认为,OpenGL的“直线”概念与数学上的“线段”接近,它能够由两个端点来肯定。
多边形是由多条线段首尾相连而造成的闭合区域。OpenGL规定,一个多边形必须是一个“凸多边形”(其定义为:多边形内任意两点所肯定的线段都在多边形内,由此也能够推导出,凸多边形不能是空心的)。多边形能够由其边的端点(这里可称为顶点)来肯定。(注意:若是使用的多边形不是凸多边形,则最后输出的效果是未定义的——OpenGL为了效率,放宽了检查,这可能致使显示错误。要避免这个错误,尽可能使用三角形,由于三角形都是凸多边形)

能够想象,经过点、直线和多边形,就能够组合成各类几何图形。甚至于,你能够把一段弧当作是不少短的直线段相连,这些直线段足够短,以致于其长度小于一个像素的宽度。这样一来弧和圆也能够表示出来了。经过位于不一样平面的相连的小多边形,咱们还能够组成一个“曲面”。

2、在OpenGL中指定顶点
由以上的讨论能够知道,“点”是一切的基础。
如何指定一个点呢?OpenGL提供了一系列函数。它们都以glVertex开头,后面跟一个数字和1~2个字母。例如:
glVertex2d
glVertex2f
glVertex3f
glVertex3fv
等等。
数字表示参数的个数,2表示有两个参数,3表示三个,4表示四个(我知道有点罗嗦~)。
字母表示参数的类型,s表示16位整数(OpenGL中将这个类型定义为GLshort),
                   i表示32位整数(OpenGL中将这个类型定义为GLint和GLsizei),
                   f表示32位浮点数(OpenGL中将这个类型定义为GLfloat和GLclampf),
                   d表示64位浮点数(OpenGL中将这个类型定义为GLdouble和GLclampd)。
                   v表示传递的几个参数将使用指针的方式,见下面的例子。
这些函数除了参数的类型和个数不一样之外,功能是相同的。例如,如下五个代码段的功能是等效的:
(一)glVertex2i(1, 3);
(二)glVertex2f(1.0f, 3.0f);
(三)glVertex3f(1.0f, 3.0f, 0.0f);
(四)glVertex4f(1.0f, 3.0f, 0.0f, 1.0f);
(五)GLfloat VertexArr3[] = {1.0f, 3.0f, 0.0f};
      glVertex3fv(VertexArr3);
之后咱们将用glVertex*来表示这一系列函数。
注意:OpenGL的不少函数都是采用这样的形式,一个相同的前缀再加上参数说明标记,这一点会随着学习的深刻而有更多的体会。


3、开始绘制
假设如今我已经指定了若干顶点,那么OpenGL是如何知道我想拿这些顶点来干什么呢?是一个一个的画出来,仍是连成线?或者构成一个多边形?或者作其它什么事情?
为了解决这一问题,OpenGL要求:指定顶点的命令必须包含在glBegin函数以后,glEnd函数以前(不然指定的顶点将被忽略)。并由glBegin来指明如何使用这些点。
例如我写:
glBegin(GL_POINTS);
     glVertex2f(0.0f, 0.0f);
     glVertex2f(0.5f, 0.0f);
glEnd();
则这两个点将分别被画出来。若是将GL_POINTS替换成GL_LINES,则两个点将被认为是直线的两个端点,OpenGL将会画出一条直线。
咱们还能够指定更多的顶点,而后画出更复杂的图形。
另外一方面,glBegin支持的方式除了GL_POINTS和GL_LINES,还有GL_LINE_STRIP,GL_LINE_LOOP,GL_TRIANGLES,GL_TRIANGLE_STRIP,GL_TRIANGLE_FAN等,每种方式的大体效果见下图:
http://blog.programfan.com/upfile/200607/200607311604018.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,因为该书的旧版(初版,1994年)已经流传于网络,我但愿没有触及到版权问题。

我并不许备在glBegin的各类方式上大做文章。你们能够本身尝试改变glBegin的方式和顶点的位置,生成一些有趣的图案。

程序代码:
void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin( /* 在这里填上你所但愿的模式 */ );
        /* 在这里使用glVertex*系列函数 */
        /* 指定你所但愿的顶点位置 */
     glEnd();
     glFlush();
}
把这段代码改为你喜欢的样子,而后用它替换第一课中的myDisplay函数,编译后便可运行。



两个例子
例1、画一个圆
/*
正四边形,正五边形,正六边形,……,直到正n边形,当n越大时,这个图形就越接近圆
当n大到必定程度后,人眼将没法把它跟真正的圆相区别
这时咱们已经成功的画出了一个“圆”
(注:画圆的方法不少,这里使用的是比较简单,但效率较低的一种)
试修改下面的const int n的值,观察当n=3,4,5,8,10,15,20,30,50等不一样数值时输出的变化状况
将GL_POLYGON改成GL_LINE_LOOP、GL_POINTS等其它方式,观察输出的变化状况
*/
#include <math.h>
const int n = 20;
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536f;
void myDisplay(void)
{
     int i;
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_POLYGON);
     for(i=0; i<n; ++i)
         glVertex2f(R*cos(2*Pi/n*i), R*sin(2*Pi/n*i));
     glEnd();
     glFlush();
}


例2、画一个五角星
/*
设五角星的五个顶点分布位置关系以下:
      A
E        B

    D    C
首先,根据余弦定理列方程,计算五角星的中心到顶点的距离a
(假设五角星对应正五边形的边长为.0)
a = 1 / (2-2*cos(72*Pi/180));
而后,根据正弦和余弦的定义,计算B的x坐标bx和y坐标by,以及C的y坐标
(假设五角星的中心在坐标原点)
bx = a * cos(18 * Pi/180);
by = a * sin(18 * Pi/180);
cy = -a * cos(18 * Pi/180);
五个点的坐标就能够经过以上四个量和一些常数简单的表示出来
*/
#include <math.h>
const GLfloat Pi = 3.1415926536f;
void myDisplay(void)
{
     GLfloat a = 1 / (2-2*cos(72*Pi/180));
     GLfloat bx = a * cos(18 * Pi/180);
     GLfloat by = a * sin(18 * Pi/180);
     GLfloat cy = -a * cos(18 * Pi/180);
     GLfloat
         PointA[2] = { 0, a },
         PointB[2] = { bx, by },
         PointC[2] = { 0.5, cy },
         PointD[2] = { -0.5, cy },
         PointE[2] = { -bx, by };

     glClear(GL_COLOR_BUFFER_BIT);
     // 按照A->C->E->B->D->A的顺序,能够一笔将五角星画出
     glBegin(GL_LINE_LOOP);
         glVertex2fv(PointA);
         glVertex2fv(PointC);
         glVertex2fv(PointE);
         glVertex2fv(PointB);
         glVertex2fv(PointD);
     glEnd();
     glFlush();
}


例3、画出正弦函数的图形
/*
因为OpenGL默认坐标值只能从-1到1,(能够修改,但方法留到之后讲)
因此咱们设置一个因子factor,把全部的坐标值等比例缩小,
这样就能够画出更多个正弦周期
试修改factor的值,观察变化状况
*/
#include <math.h>
const GLfloat factor = 0.1f;
void myDisplay(void)
{
     GLfloat x;
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_LINES);
         glVertex2f(-1.0f, 0.0f);
         glVertex2f(1.0f, 0.0f);         // 以上两个点能够画x轴
         glVertex2f(0.0f, -1.0f);
         glVertex2f(0.0f, 1.0f);         // 以上两个点能够画y轴
     glEnd();
     glBegin(GL_LINE_STRIP);
     for(x=-1.0f/factor; x<1.0f/factor; x+=0.01f)
     {
         glVertex2f(x*factor, sin(x)*factor);
     }
     glEnd();
     glFlush();
}


小结
本课讲述了点、直线和多边形的概念,以及如何使用OpenGL来描述点,并使用点来描述几何图形。
你们能够发挥本身的想象,画出各类几何图形,固然,也能够用GL_LINE_STRIP把不少位置相近的点链接起来,构成函数图象。若是有兴趣,也能够去找一些图象比较美观的函数,本身动手,用OpenGL把它画出来。

=====================    第二课 完    =====================
=====================TO BE CONTINUED=====================

OpenGL入门学习[三]

在第二课中,咱们学习了如何绘制几何图形,但你们若是多写几个程序,就会发现其实仍是有些郁闷之处。例如:点过小,难以看清楚;直线也太细,不舒服;或者想画虚线,但不知道方法只能用许多短直线,甚至用点组合而成。

这些问题将在本课中被解决。

下面就点、直线、多边形分别讨论。

一、关于点

点的大小默认为1个像素,但也能够改变之。改变的命令为glPointSize,其函数原型以下:

void glPointSize(GLfloat size);

size必须大于0.0f,默认值为1.0f,单位为“像素”。

注意:对于具体的OpenGL实现,点的大小都有个限度的,若是设置的size超过最大值,则设置可能会有问题。

例子:

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glPointSize(5.0f);

     glBegin(GL_POINTS);

         glVertex2f(0.0f, 0.0f);

         glVertex2f(0.5f, 0.5f);

     glEnd();

     glFlush();

}

二、关于直线

(1)直线能够指定宽度:

void glLineWidth(GLfloat width);

其用法跟glPointSize相似。

(2)画虚线。

首先,使用glEnable(GL_LINE_STIPPLE);来启动虚线模式(使用glDisable(GL_LINE_STIPPLE)能够关闭之)。

而后,使用glLineStipple来设置虚线的样式。

void glLineStipple(GLint factor, GLushort pattern);

pattern是由1和0组成的长度为16的序列,从最低位开始看,若是为1,则直线上接下来应该画的factor个点将被画为实的;若是为0,则直线上接下来应该画的factor个点将被画为虚的。

如下是一些例子:

声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,因为该书的旧版(初版,1994年)已经流传于网络,我但愿没有触及到版权问题。

示例代码:

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glEnable(GL_LINE_STIPPLE);

     glLineStipple(2, 0x0F0F);

     glLineWidth(10.0f);

     glBegin(GL_LINES);

         glVertex2f(0.0f, 0.0f);

         glVertex2f(0.5f, 0.5f);

     glEnd();

     glFlush();

}

三、关于多边形

多边形的内容较多,咱们将讲述如下四个方面。

(1)多边形的两面以及绘制方式。

虽然咱们目前尚未真正的使用三维坐标来画图,可是创建一些三维的概念仍是必要的。

从三维的角度来看,一个多边形具备两个面。每个面均可以设置不一样的绘制方式:填充、只绘制边缘轮廓线、只绘制顶点,其中“填充”是默认的方式。能够为两个面分别设置不一样的方式。

glPolygonMode(GL_FRONT, GL_FILL);            // 设置正面为填充方式

glPolygonMode(GL_BACK, GL_LINE);             // 设置反面为边缘绘制方式

glPolygonMode(GL_FRONT_AND_BACK, GL_POINT); // 设置两面均为顶点绘制方式

(2)反转

通常约定为“顶点以逆时针顺序出如今屏幕上的面”为“正面”,另外一个面即成为“反面”。生活中常见的物体表面,一般均可以用这样的“正面”和“反面”,“合理的”被表现出来(请找一个比较透明的矿泉水瓶子,在正对你的一面沿逆时针画一个圆,并标明画的方向,而后将背面转为正面,画一个相似的圆,体会一下“正面”和“反面”。你会发现正对你的方向,瓶的外侧是正面,而背对你的方向,瓶的内侧才是正面。正对你的内侧和背对你的外侧则是反面。这样一来,一样属于“瓶的外侧”这个表面,但某些地方算是正面,某些地方却算是反面了)。

但也有一些表面比较特殊。例如“麦比乌斯带”(请本身Google一下),能够所有使用“正面”或所有使用“背面”来表示。

能够经过glFrontFace函数来交换“正面”和“反面”的概念。

glFrontFace(GL_CCW);   // 设置CCW方向为“正面”,CCW即CounterClockWise,逆时针

glFrontFace(GL_CW);    // 设置CW方向为“正面”,CW即ClockWise,顺时针

下面是一个示例程序,请用它替换第一课中的myDisplay函数,并将glFrontFace(GL_CCW)修改成glFrontFace(GL_CW),并观察结果的变化。

void myDisplay(void)

{

     glClear(GL_COLOR_BUFFER_BIT);

     glPolygonMode(GL_FRONT, GL_FILL); // 设置正面为填充模式

     glPolygonMode(GL_BACK, GL_LINE);   // 设置反面为线形模式

     glFrontFace(GL_CCW);               // 设置逆时针方向为正面

     glBegin(GL_POLYGON);               // 按逆时针绘制一个正方形,在左下方

         glVertex2f(-0.5f, -0.5f);

         glVertex2f(0.0f, -0.5f);

         glVertex2f(0.0f, 0.0f);

         glVertex2f(-0.5f, 0.0f);

     glEnd();

     glBegin(GL_POLYGON);               // 按顺时针绘制一个正方形,在右上方

         glVertex2f(0.0f, 0.0f);

         glVertex2f(0.0f, 0.5f);

         glVertex2f(0.5f, 0.5f);

         glVertex2f(0.5f, 0.0f);

     glEnd();

     glFlush();

}

(3)剔除多边形表面

在三维空间中,一个多边形虽然有两个面,但咱们没法看见背面的那些多边形,而一些多边形虽然是正面的,但被其余多边形所遮挡。若是将没法看见的多边形和可见的多边形同等对待,无疑会下降咱们处理图形的效率。在这种时候,能够将没必要要的面剔除。

首先,使用glEnable(GL_CULL_FACE);来启动剔除功能(使用glDisable(GL_CULL_FACE)能够关闭之)

而后,使用glCullFace来进行剔除。

glCullFace的参数能够是GL_FRONT,GL_BACK或者GL_FRONT_AND_BACK,分别表示剔除正面、剔除反面、剔除正反两面的多边形。

注意:剔除功能只影响多边形,而对点和直线无影响。例如,使用glCullFace(GL_FRONT_AND_BACK)后,全部的多边形都将被剔除,因此看见的就只有点和直线。

(4)镂空多边形

直线能够被画成虚线,而多边形则能够进行镂空。

首先,使用glEnable(GL_POLYGON_STIPPLE);来启动镂空模式(使用glDisable(GL_POLYGON_STIPPLE)能够关闭之)。

而后,使用glPolygonStipple来设置镂空的样式。

void glPolygonStipple(const GLubyte *mask);

其中的参数mask指向一个长度为128字节的空间,它表示了一个32*32的矩形应该如何镂空。其中:第一个字节表示了最左下方的从左到右(也能够是从右到左,这个能够修改)8个像素是否镂空(1表示不镂空,显示该像素;0表示镂空,显示其后面的颜色),最后一个字节表示了最右上方的8个像素是否镂空。

可是,若是咱们直接定义这个mask数组,像这样:

static GLubyte Mask[128] =

{

     0x00, 0x00, 0x00, 0x00,    //   这是最下面的一行

     0x00, 0x00, 0x00, 0x00,

     0x03, 0x80, 0x01, 0xC0,    //   麻

     0x06, 0xC0, 0x03, 0x60,    //   烦

     0x04, 0x60, 0x06, 0x20,    //   的

     0x04, 0x30, 0x0C, 0x20,    //   初

     0x04, 0x18, 0x18, 0x20,    //   始

     0x04, 0x0C, 0x30, 0x20,    //   化

     0x04, 0x06, 0x60, 0x20,    //   ,

     0x44, 0x03, 0xC0, 0x22,    //   不

     0x44, 0x01, 0x80, 0x22,    //   建

     0x44, 0x01, 0x80, 0x22,    //   议

     0x44, 0x01, 0x80, 0x22,    //   使

     0x44, 0x01, 0x80, 0x22,    //   用

     0x44, 0x01, 0x80, 0x22,

     0x44, 0x01, 0x80, 0x22,

     0x66, 0x01, 0x80, 0x66,

     0x33, 0x01, 0x80, 0xCC,

     0x19, 0x81, 0x81, 0x98,

     0x0C, 0xC1, 0x83, 0x30,

     0x07, 0xE1, 0x87, 0xE0,

     0x03, 0x3F, 0xFC, 0xC0,

     0x03, 0x31, 0x8C, 0xC0,

     0x03, 0x3F, 0xFC, 0xC0,

     0x06, 0x64, 0x26, 0x60,

     0x0C, 0xCC, 0x33, 0x30,

     0x18, 0xCC, 0x33, 0x18,

     0x10, 0xC4, 0x23, 0x08,

     0x10, 0x63, 0xC6, 0x08,

     0x10, 0x30, 0x0C, 0x08,

     0x10, 0x18, 0x18, 0x08,

     0x10, 0x00, 0x00, 0x08    // 这是最上面的一行

};

这样一堆数据很是缺少直观性,咱们须要很费劲的去分析,才会发现它表示的居然是一只苍蝇。

若是将这样的数据保存成图片,并用专门的工具进行编辑,显然会方便不少。下面介绍如何作到这一点。

首先,用Windows自带的画笔程序新建一副图片,取名为mask.bmp,注意保存时,应该选择“单色位图”。在“图象”->“属性”对话框中,设置图片的高度和宽度均为32。

用放大镜观察图片,并编辑之。黑色对应二进制零(镂空),白色对应二进制一(不镂空),编辑完毕后保存。

而后,就能够使用如下代码来得到这个Mask数组了。

static GLubyte Mask[128];

FILE *fp;

fp = fopen("mask.bmp", "rb");

if( !fp )

     exit(0);

// 移动文件指针到这个位置,使得再读sizeof(Mask)个字节就会遇到文件结束

// 注意-(int)sizeof(Mask)虽然不是什么好的写法,但这里它确实是正确有效的

// 若是直接写-sizeof(Mask)的话,由于sizeof取得的是一个无符号数,取负号会有问题

if( fseek(fp, -(int)sizeof(Mask), SEEK_END) )

     exit(0);

// 读取sizeof(Mask)个字节到Mask

if( !fread(Mask, sizeof(Mask), 1, fp) )

     exit(0);

fclose(fp);

好的,如今请本身编辑一个图片做为mask,并用上述方法取得Mask数组,运行后观察效果。

说明:绘制虚线时能够设置factor因子,但多边形的镂空没法设置factor因子。请用鼠标改变窗口的大小,观察镂空效果的变化状况。

#include <stdio.h>

#include <stdlib.h>

void myDisplay(void)

{

     static GLubyte Mask[128];

     FILE *fp;

     fp = fopen("mask.bmp", "rb");

     if( !fp )

         exit(0);

     if( fseek(fp, -(int)sizeof(Mask), SEEK_END) )

         exit(0);

     if( !fread(Mask, sizeof(Mask), 1, fp) )

         exit(0);

     fclose(fp);

     glClear(GL_COLOR_BUFFER_BIT);

     glEnable(GL_POLYGON_STIPPLE);

     glPolygonStipple(Mask);

     glRectf(-0.5f, -0.5f, 0.0f, 0.0f);   // 在左下方绘制一个有镂空效果的正方形

     glDisable(GL_POLYGON_STIPPLE);

     glRectf(0.0f, 0.0f, 0.5f, 0.5f);     // 在右上方绘制一个无镂空效果的正方形

     glFlush();

}

小结

本课学习了绘制几何图形的一些细节。

点能够设置大小。

直线能够设置宽度;能够将直线画成虚线。

多边形的两个面的绘制方法能够分别设置;在三维空间中,不可见的多边形能够被剔除;能够将填充多边形绘制成镂空的样式。

了解这些细节会使咱们在一些图象绘制中更加驾轻就熟。

另外,把一些数据写到程序以外的文件中,并用专门的工具编辑之,有时能够显得更方便。

=====================    第三课 完    =====================

=====================TO BE CONTINUED=====================






OpenGL入门学习[四]
2008-10-06 21:26
本次学习的是颜色的选择。终于要走出黑白的世界了~~


OpenGL支持两种颜色模式:一种是RGBA,一种是颜色索引模式。
不管哪一种颜色模式,计算机都必须为每个像素保存一些数据。不一样的是,RGBA模式中,数据直接就表明了颜色;而颜色索引模式中,数据表明的是一个索引,要获得真正的颜色,还必须去查索引表。

1. RGBA颜色
RGBA模式中,每个像素会保存如下数据:R值(红色份量)、G值(绿色份量)、B值(蓝色份量)和A值(alpha份量)。其中红、绿、蓝三种颜色相组合,就能够获得咱们所须要的各类颜色,而alpha不直接影响颜色,它将留待之后介绍。
在RGBA模式下选择颜色是十分简单的事情,只须要一个函数就能够搞定。
glColor*系列函数能够用于设置颜色,其中三个参数的版本能够指定R、G、B的值,而A值采用默认;四个参数的版本能够分别指定R、G、B、A的值。例如:
void glColor3f(GLfloat red, GLfloat green, GLfloat blue);
void glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
(还记得吗?3f表示有三个浮点参数~请看第二课中关于glVertex*函数的叙述。)
将浮点数做为参数,其中0.0表示不使用该种颜色,而1.0表示将该种颜色用到最多。例如:
glColor3f(1.0f, 0.0f, 0.0f);    表示不使用绿、蓝色,而将红色使用最多,因而获得最纯净的红色。
glColor3f(0.0f, 1.0f, 1.0f);    表示使用绿、蓝色到最多,而不使用红色。混合的效果就是浅蓝色。
glColor3f(0.5f, 0.5f, 0.5f);    表示各类颜色使用一半,效果为灰色。
注意:浮点数能够精确到小数点后若干位,这并不表示计算机就能够显示如此多种颜色。实际上,计算机能够显示的颜色种数将由硬件决定。若是OpenGL找不到精确的颜色,会进行相似“四舍五入”的处理。

你们能够经过改变下面代码中glColor3f的参数值,绘制不一样颜色的矩形。
void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);
     glColor3f(0.0f, 1.0f, 1.0f);
     glRectf(-0.5f, -0.5f, 0.5f, 0.5f);
     glFlush();
}

注意:glColor系列函数,在参数类型不一样时,表示“最大”颜色的值也不一样。
采用f和d作后缀的函数,以1.0表示最大的使用。
采用b作后缀的函数,以127表示最大的使用。
采用ub作后缀的函数,以255表示最大的使用。
采用s作后缀的函数,以32767表示最大的使用。
采用us作后缀的函数,以65535表示最大的使用。
这些规则看似麻烦,但熟悉后实际使用中不会有什么障碍。

二、索引颜色
在索引颜色模式中,OpenGL须要一个颜色表。这个表就至关于画家的调色板:虽然能够调出不少种颜色,但同时存在于调色板上的颜色种数将不会超过调色板的格数。试将颜色表的每一项想象成调色板上的一个格子:它保存了一种颜色。
在使用索引颜色模式画图时,我说“我把第i种颜色设置为某某”,其实就至关于将调色板的第i格调为某某颜色。“我须要第k种颜色来画图”,那么就用画笔去蘸一下第k格调色板。
颜色表的大小是颇有限的,通常在256~4096之间,且老是2的整数次幂。在使用索引颜色方式进行绘图时,老是先设置颜色表,而后选择颜色。

2.一、选择颜色
使用glIndex*系列函数能够在颜色表中选择颜色。其中最经常使用的多是glIndexi,它的参数是一个整形。
void glIndexi(GLint c);
是的,这的确很简单。

2.二、设置颜色表
OpenGL 并直接没有提供设置颜色表的方法,所以设置颜色表须要使用操做系统的支持。咱们所用的Windows和其余大多数图形操做系统都具备这个功能,但所使用的函数却不相同。正如我没有讲述如何本身写代码在Windows下创建一个窗口,这里我也不会讲述如何在Windows下设置颜色表。
GLUT工具包提供了设置颜色表的函数glutSetColor,但我测试始终有问题。如今为了让你们体验一下索引颜色,我向你们介绍另外一个OpenGL工具包: aux。这个工具包是VisualStudio自带的,没必要另外安装,但它已通过时,这里仅仅是体验一下,你们没必要深刻。
#include <windows.h>
#include <GL/gl.h>
#include <GL/glaux.h>

#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glaux.lib")

#include <math.h>
const GLdouble Pi = 3.1415926536;
void myDisplay(void)
{
     int i;
     for(i=0; i<8; ++i)
         auxSetOneColor(i, (float)(i&0x04), (float)(i&0x02), (float)(i&0x01));
     glShadeModel(GL_FLAT);
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_TRIANGLE_FAN);
     glVertex2f(0.0f, 0.0f);
     for(i=0; i<=8; ++i)
     {
         glIndexi(i);
         glVertex2f(cos(i*Pi/4), sin(i*Pi/4));
     }
     glEnd();
     glFlush();
}

int main(void)
{
     auxInitDisplayMode(AUX_SINGLE|AUX_INDEX);
     auxInitPosition(0, 0, 400, 400);
     auxInitWindow(L"");
     myDisplay();
     Sleep(10 * 1000);
     return 0;
}

其它部分你们均可以无论,只看myDisplay函数就能够了。首先,使用auxSetOneColor设置颜色表中的一格。循环八次就能够设置八格。
glShadeModel等下再讲,这里不提。
而后在循环中用glVertex设置顶点,同时用glIndexi改变顶点表明的颜色。
最终获得的效果是八个相同形状、不一样颜色的三角形。

索引颜色虽然讲得多了点。索引颜色的主要优点是占用空间小(每一个像素没必要单独保存本身的颜色,只用不多的二进制位就能够表明其颜色在颜色表中的位置),花费系统资源少,图形运算速度快,但它编程稍稍显得不是那么方便,而且画面效果也会比RGB颜色差一些。“星际争霸”可能表明了256色的颜色表的画面效果,虽然它在一台很烂的PC上也能够运行很流畅,但以目前的眼光来看,其画面效果就显得不足了。
目前的PC机性能已经足够在各类场合下使用RGB颜色,所以PC程序开发中,使用索引颜色已经不是主流。固然,一些小型设备例如GBA、手机等,索引颜色仍是有它的用武之地。


三、指定清除屏幕用的颜色
咱们写:glClear(GL_COLOR_BUFFER_BIT);意思是把屏幕上的颜色清空。
但实际上什么才叫“空”呢?在宇宙中,黑色表明了“空”;在一张白纸上,白色表明了“空”;在信封上,信封的颜色才是“空”。
OpenGL用下面的函数来定义清楚屏幕后屏幕所拥有的颜色。
在RGB模式下,使用glClearColor来指定“空”的颜色,它须要四个参数,其参数的意义跟glColor4f类似。
在索引颜色模式下,使用glClearIndex来指定“空”的颜色所在的索引,它须要一个参数,其意义跟glIndexi类似。
void myDisplay(void)
{
     glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
     glClear(GL_COLOR_BUFFER_BIT);
     glFlush();
}
呵,这个还真简单~


四、指定着色模型
OpenGL容许为同一多边形的不一样顶点指定不一样的颜色。例如:
#include <math.h>
const GLdouble Pi = 3.1415926536;
void myDisplay(void)
{
     int i;
     // glShadeModel(GL_FLAT);
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_TRIANGLE_FAN);
     glColor3f(1.0f, 1.0f, 1.0f);
     glVertex2f(0.0f, 0.0f);
     for(i=0; i<=8; ++i)
     {
         glColor3f(i&0x04, i&0x02, i&0x01);
         glVertex2f(cos(i*Pi/4), sin(i*Pi/4));
     }
     glEnd();
     glFlush();
}
在默认状况下,OpenGL会计算两点顶点之间的其它点,并为它们填上“合适”的颜色,使相邻的点的颜色值都比较接近。若是使用的是RGB模式,看起来就具备渐变的效果。若是是使用颜色索引模式,则其相邻点的索引值是接近的,若是将颜色表中接近的项设置成接近的颜色,则看起来也是渐变的效果。但若是颜色表中接近的项颜色却差距很大,则看起来多是很奇怪的效果。
使用glShadeModel函数能够关闭这种计算,若是顶点的颜色不一样,则将顶点之间的其它点所有设置为与某一个点相同。(直线之后指定的点的颜色为准,而多边形将以任意顶点的颜色为准,由实现决定。)为了不这个不肯定性,尽可能在多边形中使用同一种颜色。
glShadeModel的使用方法:
glShadeModel(GL_SMOOTH);    // 平滑方式,这也是默认方式
glShadeModel(GL_FLAT);      // 单色方式

小结:
本课学习了如何设置颜色。其中RGB颜色方式是目前PC机上的经常使用方式。
能够设置glClear清除后屏幕所剩的颜色。
能够设置颜色填充方式:平滑方式或单色方式。

=====================    第四课 完    =====================
=====================TO BE CONTINUED=====================



OpenGL入门学习[五]



今天要讲的是三维变换的内容,课程比较枯燥。主要是由于不少函数在单独使用时都很差描述其效果,我只好在最后举一个比较综合的例子。但愿你们能一口气看到底了。只看一次可能不够,若是感受到迷糊,不妨多看两遍。有疑问能够在下面跟帖提出。
我也使用了若干图形,但愿能够帮助理解。


在前面绘制几何图形的时候,你们是否以为咱们绘图的范围太狭隘了呢?坐标只能从-1到1,还只能是X轴向右,Y轴向上,Z轴垂直屏幕。这些限制给咱们的绘图带来了不少不便。

咱们生活在一个三维的世界——若是要观察一个物体,咱们能够:
一、从不一样的位置去观察它。(视图变换)
二、移动或者旋转它,固然了,若是它只是计算机里面的物体,咱们还能够放大或缩小它。(模型变换)
三、若是把物体画下来,咱们能够选择:是否须要一种“近大远小”的透视效果。另外,咱们可能只但愿看到物体的一部分,而不是所有(剪裁)。(投影变换)
四、咱们可能但愿把整个看到的图形画下来,但它只占据纸张的一部分,而不是所有。(视口变换)
这些,均可以在OpenGL中实现。

OpenGL变换其实是经过矩阵乘法来实现。不管是移动、旋转仍是缩放大小,都是经过在当前矩阵的基础上乘以一个新的矩阵来达到目的。关于矩阵的知识,这里不详细介绍,有兴趣的朋友能够看看线性代数(大学生的话多半应该学过的)。
OpenGL能够在最底层直接操做矩阵,不过做为初学,这样作的意义并不大。这里就不作介绍了。


一、模型变换和视图变换
从“相对移动”的观点来看,改变观察点的位置与方向和改变物体自己的位置与方向具备等效性。在OpenGL中,实现这两种功能甚至使用的是一样的函数。
因为模型和视图的变换都经过矩阵运算来实现,在进行变换前,应先设置当前操做的矩阵为“模型视图矩阵”。设置的方法是以GL_MODELVIEW为参数调用glMatrixMode函数,像这样:
glMatrixMode(GL_MODELVIEW);
一般,咱们须要在进行变换前把当前矩阵设置为单位矩阵。这也只须要一行代码:
glLoadIdentity();

而后,就能够进行模型变换和视图变换了。进行模型和视图变换,主要涉及到三个函数:
glTranslate*,把当前矩阵和一个表示移动物体的矩阵相乘。三个参数分别表示了在三个坐标上的位移值。
glRotate*,把当前矩阵和一个表示旋转物体的矩阵相乘。物体将绕着(0,0,0)到(x,y,z)的直线以逆时针旋转,参数angle表示旋转的角度。
glScale*,把当前矩阵和一个表示缩放物体的矩阵相乘。x,y,z分别表示在该方向上的缩放比例。

注意我都是说“与XX相乘”,而不是直接说“这个函数就是旋转”或者“这个函数就是移动”,这是有缘由的,立刻就会讲到。
假设当前矩阵为单位矩阵,而后先乘以一个表示旋转的矩阵R,再乘以一个表示移动的矩阵T,最后获得的矩阵再乘上每个顶点的坐标矩阵v。因此,通过变换获得的顶点坐标就是((RT)v)。因为矩阵乘法的结合率,((RT)v) = (R(Tv)),换句话说,其实是先进行移动,而后进行旋转。即:实际变换的顺序与代码中写的顺序是相反的。因为“先移动后旋转”和“先旋转后移动”获得的结果极可能不一样,初学的时候须要特别注意这一点。
OpenGL之因此这样设计,是为了获得更高的效率。但在绘制复杂的三维图形时,若是每次都去考虑如何把变换倒过来,也是很痛苦的事情。这里介绍另外一种思路,可让代码看起来更天然(写出的代码其实彻底同样,只是考虑问题时用的方法不一样了)。
让咱们想象,坐标并非固定不变的。旋转的时候,坐标系统随着物体旋转。移动的时候,坐标系统随着物体移动。如此一来,就不须要考虑代码的顺序反转的问题了。

以上都是针对改变物体的位置和方向来介绍的。若是要改变观察点的位置,除了配合使用glRotate*和glTranslate*函数之外,还能够使用这个函数:gluLookAt。它的参数比较多,前三个参数表示了观察点的位置,中间三个参数表示了观察目标的位置,最后三个参数表明从(0,0,0)到 (x,y,z)的直线,它表示了观察者认为的“上”方向。


二、投影变换

投影变换就是定义一个可视空间,可视空间之外的物体不会被绘制到屏幕上。(注意,从如今起,坐标能够再也不是-1.0到1.0了!)
OpenGL支持两种类型的投影变换,即透视投影和正投影。投影也是使用矩阵来实现的。若是须要操做投影矩阵,须要以GL_PROJECTION为参数调用glMatrixMode函数。
glMatrixMode(GL_PROJECTION);
一般,咱们须要在进行变换前把当前矩阵设置为单位矩阵。
glLoadIdentity();

透视投影所产生的结果相似于照片,有近大远小的效果,好比在火车头内向前照一个铁轨的照片,两条铁轨彷佛在远处相交了。
使用glFrustum函数能够将当前的可视空间设置为透视投影空间。其参数的意义以下图:
http://blog.programfan.com/upfile/200610/20061007151547.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,因为该书的旧版(初版,1994年)已经流传于网络,我但愿没有触及到版权问题。
也能够使用更经常使用的gluPerspective函数。其参数的意义以下图:
http://blog.programfan.com/upfile/200610/2006100715161.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,因为该书的旧版(初版,1994年)已经流传于网络,我但愿没有触及到版权问题。

正投影至关于在无限远处观察获得的结果,它只是一种理想状态。但对于计算机来讲,使用正投影有可能得到更好的运行速度。
使用glOrtho函数能够将当前的可视空间设置为正投影空间。其参数的意义以下图:
http://blog.programfan.com/upfile/200610/20061007151619.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,因为该书的旧版(初版,1994年)已经流传于网络,我但愿没有触及到版权问题。

若是绘制的图形空间自己就是二维的,能够使用gluOrtho2D。他的使用相似于glOrgho。


三、视口变换
当一切工做已经就绪,只须要把像素绘制到屏幕上了。这时候还剩最后一个问题:应该把像素绘制到窗口的哪一个区域呢?一般状况下,默认是完整的填充整个窗口,但咱们彻底能够只填充一半。(即:把整个图象填充到一半的窗口内)
http://blog.programfan.com/upfile/200610/20061007151639.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,因为该书的旧版(初版,1994年)已经流传于网络,我但愿没有触及到版权问题。

使用glViewport来定义视口。其中前两个参数定义了视口的左下脚(0,0表示最左下方),后两个参数分别是宽度和高度。

四、操做矩阵堆栈
介因而入门教程,先简单介绍一下堆栈。你能够把堆栈想象成一叠盘子。开始的时候一个盘子也没有,你能够一个一个往上放,也能够一个一个取下来。每次取下的,都是最后一次被放上去的盘子。一般,在计算机实现堆栈时,堆栈的容量是有限的,若是盘子过多,就会出错。固然,若是没有盘子了,再要求取一个盘子,也会出错。
咱们在进行矩阵操做时,有可能须要先保存某个矩阵,过一段时间再恢复它。当咱们须要保存时,调用glPushMatrix函数,它至关于把矩阵(至关于盘子)放到堆栈上。当须要恢复最近一次的保存时,调用glPopMatrix函数,它至关于把矩阵从堆栈上取下。OpenGL规定堆栈的容量至少能够容纳32个矩阵,某些OpenGL实现中,堆栈的容量实际上超过了32个。所以没必要过于担忧矩阵的容量问题。
一般,用这种先保存后恢复的措施,比先变换再逆变换要更方便,更快速。
注意:模型视图矩阵和投影矩阵都有相应的堆栈。使用glMatrixMode来指定当前操做的到底是模型视图矩阵仍是投影矩阵。

五、综合举例
好了,视图变换的入门知识差很少就讲完了。但咱们不能就这样结束。由于本次课程的内容实在过于枯燥,若是分别举例,可能效果不佳。我只好综合的讲一个例子,算是给你们一个参考。至于实际的掌握,还要靠你们本身花功夫。闲话少说,如今进入正题。

咱们要制做的是一个三维场景,包括了太阳、地球和月亮。假定一年有12个月,每月30天。每一年,地球绕着太阳转一圈。每月,月亮围着地球转一圈。即一年有360天。如今给出日期的编号(0~359),要求绘制出太阳、地球、月亮的相对位置示意图。(这是为了编程方便才这样设计的。若是须要制做更现实的状况,那也只是一些数值处理而已,与OpenGL关系不大)
首先,让咱们认定这三个天体都是球形,且他们的运动轨迹处于同一水平面,创建如下坐标系:太阳的中心为原点,天体轨迹所在的平面表示了X轴与Y轴决定的平面,且每一年第一天,地球在X轴正方向上,月亮在地球的正X轴方向。
下一步是确立可视空间。注意:太阳的半径要比太阳到地球的距离短得多。若是咱们直接使用天文观测获得的长度比例,则当整个窗口表示地球轨道大小时,太阳的大小将被忽略。所以,咱们只能成倍的放大几个天体的半径,以适应咱们观察的须要。(百度一下,获得太阳、地球、月亮的大体半径分别是:696000km, 6378km,1738km。地球到太阳的距离约为1.5亿km=150000000km,月亮到地球的距离约为380000km。)
让咱们假想一些数据,将三个天体的半径分别“修改”为:69600000(放大100倍),15945000(放大2500倍),4345000(放大5000倍)。将地球到月亮的距离“修改”为38000000(放大100倍)。地球到太阳的距离保持不变。
为了让地球和月亮在离咱们很近时,咱们仍然不须要变换观察点和观察方向就能够观察它们,咱们把观察点放在这个位置:(0, -200000000, 0) ——由于地球轨道半径为150000000,我们就凑个整,取-200000000就能够了。观察目标设置为原点(即太阳中心),选择Z轴正方向做为 “上”方。固然咱们还能够把观察点往“上”方移动一些,获得(0, -200000000, 200000000),这样能够获得45度角的俯视效果。
为了获得透视效果,咱们使用gluPerspective来设置可视空间。假定可视角为60度(若是调试时发现该角度不合适,可修改之。我在最后选择的数值是75。),高宽比为1.0。最近可视距离为1.0,最远可视距离为200000000*2=400000000。即:gluPerspective (60, 1, 1, 400000000);


五、综合举例
好了,视图变换的入门知识差很少就讲完了。但咱们不能就这样结束。由于本次课程的内容实在过于枯燥,若是分别举例,可能效果不佳。我只好综合的讲一个例子,算是给你们一个参考。至于实际的掌握,还要靠你们本身花功夫。闲话少说,如今进入正题。

咱们要制做的是一个三维场景,包括了太阳、地球和月亮。假定一年有12个月,每月30天。每一年,地球绕着太阳转一圈。每月,月亮围着地球转一圈。即一年有360天。如今给出日期的编号(0~359),要求绘制出太阳、地球、月亮的相对位置示意图。(这是为了编程方便才这样设计的。若是须要制做更现实的状况,那也只是一些数值处理而已,与OpenGL关系不大)
首先,让咱们认定这三个天体都是球形,且他们的运动轨迹处于同一水平面,创建如下坐标系:太阳的中心为原点,天体轨迹所在的平面表示了X轴与Y轴决定的平面,且每一年第一天,地球在X轴正方向上,月亮在地球的正X轴方向。
下一步是确立可视空间。注意:太阳的半径要比太阳到地球的距离短得多。若是咱们直接使用天文观测获得的长度比例,则当整个窗口表示地球轨道大小时,太阳的大小将被忽略。所以,咱们只能成倍的放大几个天体的半径,以适应咱们观察的须要。(百度一下,获得太阳、地球、月亮的大体半径分别是:696000km, 6378km,1738km。地球到太阳的距离约为1.5亿km=150000000km,月亮到地球的距离约为380000km。)
让咱们假想一些数据,将三个天体的半径分别“修改”为:69600000(放大100倍),15945000(放大2500倍),4345000(放大2500倍)。将地球到月亮的距离“修改”为38000000(放大100倍)。地球到太阳的距离保持不变。
为了让地球和月亮在离咱们很近时,咱们仍然不须要变换观察点和观察方向就能够观察它们,咱们把观察点放在这个位置:(0, -200000000, 0) ——由于地球轨道半径为150000000,我们就凑个整,取-200000000就能够了。观察目标设置为原点(即太阳中心),选择Z轴正方向做为 “上”方。固然咱们还能够把观察点往“上”方移动一些,获得(0, -200000000, 200000000),这样能够获得45度角的俯视效果。
为了获得透视效果,咱们使用gluPerspective来设置可视空间。假定可视角为60度(若是调试时发现该角度不合适,可修改之。我在最后选择的数值是75。),高宽比为1.0。最近可视距离为1.0,最远可视距离为200000000*2=400000000。即:gluPerspective (60, 1, 1, 400000000);


如今咱们来看看如何绘制这三个天体。
为了简单起见,咱们把三个天体都想象成规则的球体。而咱们所使用的glut实用工具中,正好就有一个绘制球体的现成函数:glutSolidSphere,这个函数在“原点”绘制出一个球体。因为坐标是能够经过glTranslate*和glRotate*两个函数进行随意变换的,因此咱们就能够在任意位置绘制球体了。函数有三个参数:第一个参数表示球体的半径,后两个参数表明了“面”的数目,简单点说就是球体的精确程度,数值越大越精确,固然代价就是速度越缓慢。这里咱们只是简单的设置后两个参数为20。
太阳在坐标原点,因此不须要通过任何变换,直接绘制就能够了。
地球则要复杂一点,须要变换坐标。因为今年已经通过的天数已知为day,则地球转过的角度为day/一年的天数*360度。前面已经假定每一年都是360天,所以地球转过的角度刚好为day。因此能够经过下面的代码来解决:
glRotatef(day, 0, 0, -1);
/* 注意地球公转是“自西向东”的,所以是饶着Z轴负方向进行逆时针旋转 */
glTranslatef(地球轨道半径, 0, 0);
glutSolidSphere(地球半径, 20, 20);
月亮是最复杂的。由于它不只要绕地球转,还要随着地球绕太阳转。但若是咱们选择地球做为参考,则月亮进行的运动就是一个简单的圆周运动了。若是咱们先绘制地球,再绘制月亮,则只须要进行与地球相似的变换:
glRotatef(月亮旋转的角度, 0, 0, -1);
glTranslatef(月亮轨道半径, 0, 0);
glutSolidSphere(月亮半径, 20, 20);
但这个“月亮旋转的角度”,并不能简单的理解为day/一个月的天数30*360度。由于咱们在绘制地球时,这个坐标已是旋转过的。如今的旋转是在之前的基础上进行旋转,所以还须要处理这个“差值”。咱们能够写成:day/30*360 - day,即减去原来已经转过的角度。这只是一种简单的处理,固然也能够在绘制地球前用glPushMatrix保存矩阵,绘制地球后用glPopMatrix恢复矩阵。再设计一个跟地球位置无关的月亮位置公式,来绘制月亮。一般后一种方法比前一种要好,由于浮点的运算是不精确的,便是说咱们计算地球自己的位置就是不精确的。拿这个不精确的数去计算月亮的位置,会致使 “不精确”的成分累积,过多的“不精确”会形成错误。咱们这个小程序没有去考虑这个,但并非说这个问题不重要。
还有一个须要注意的细节: OpenGL把三维坐标中的物体绘制到二维屏幕,绘制的顺序是按照代码的顺序来进行的。所以后绘制的物体会遮住先绘制的物体,即便后绘制的物体在先绘制的物体的“后面”也是如此。使用深度测试能够解决这一问题。使用的方法是:一、以GL_DEPTH_TEST为参数调用glEnable函数,启动深度测试。二、在必要时(一般是每次绘制画面开始时),清空深度缓冲,即:glClear(GL_DEPTH_BUFFER_BIT);其中,glClear (GL_COLOR_BUFFER_BIT)与glClear(GL_DEPTH_BUFFER_BIT)能够合并写为:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
且后者的运行速度可能比前者快。


到此为止,咱们终于能够获得整个“太阳,地球和月亮”系统的完整代码。


Code:
--------------------------------------------------------------------------------
// 太阳、地球和月亮
// 假设每月都是30天
// 一年12个月,共是360天
static int day = 200; // day的变化:从0到359
void myDisplay(void)
{
     glEnable(GL_DEPTH_TEST);
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(75, 1, 1, 400000000);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1);

     // 绘制红色的“太阳”
     glColor3f(1.0f, 0.0f, 0.0f);
     glutSolidSphere(69600000, 20, 20);
     // 绘制蓝色的“地球”
     glColor3f(0.0f, 0.0f, 1.0f);
     glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(150000000, 0.0f, 0.0f);
     glutSolidSphere(15945000, 20, 20);
     // 绘制黄色的“月亮”
     glColor3f(1.0f, 1.0f, 0.0f);
     glRotatef(day/30.0*360.0 - day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(38000000, 0.0f, 0.0f);
     glutSolidSphere(4345000, 20, 20);

     glFlush();
}
--------------------------------------------------------------------------------



试修改day的值,看看画面有何变化。


小结:本课开始,咱们正式进入了三维的OpenGL世界。
OpenGL经过矩阵变换来把三维物体转变为二维图象,进而在屏幕上显示出来。为了指定当前操做的是何种矩阵,咱们使用了函数glMatrixMode。
咱们能够移动、旋转观察点或者移动、旋转物体,使用的函数是glTranslate*和glRotate*。
咱们能够缩放物体,使用的函数是glScale*。
咱们能够定义可视空间,这个空间能够是“正投影”的(使用glOrtho或gluOrtho2D),也能够是“透视投影”的(使用glFrustum或gluPerspective)。
咱们能够定义绘制到窗口的范围,使用的函数是glViewport。
矩阵有本身的“堆栈”,方便进行保存和恢复。这在绘制复杂图形时颇有帮助。使用的函数是glPushMatrix和glPopMatrix。

好了,艰苦的一课终于完毕。我知道,本课的内容十分枯燥,就连最后的例子也是。但我也没有更好的办法了,但愿你们能坚持过去。没必要担忧,熟悉本课内容后,之后的一段时间内,都会是比较轻松愉快的了。

=====================    第五课 完    =====================
=====================TO BE CONTINUED=====================



OpenGL入门学习[六]


今天要讲的是动画制做——多是各位都很喜欢的。除了讲授知识外,咱们还会让昨天那个“太阳、地球和月亮”天体图画动起来。缓和一下枯燥的气氛。


本次课程,咱们将进入激动人心的计算机动画世界。

想必你们都知道电影和动画的工做原理吧?是的,快速的把看似连续的画面一幅幅的呈如今人们面前。一旦每秒钟呈现的画面超过24幅,人们就会错觉得它是连续的。
咱们一般观看的电视,每秒播放25或30幅画面。但对于计算机来讲,它能够播放更多的画面,以达到更平滑的效果。若是速度过慢,画面不够平滑。若是速度过快,则人眼未必就能反应得过来。对于一个正常人来讲,每秒60~120幅图画是比较合适的。具体的数值因人而异。

假设某动画一共有n幅画面,则它的工做步骤就是:
显示第1幅画面,而后等待一小段时间,直到下一个1/24秒
显示第2幅画面,而后等待一小段时间,直到下一个1/24秒
……
显示第n幅画面,而后等待一小段时间,直到下一个1/24秒
结束
若是用C语言伪代码来描述这一过程,就是:
for(i=0; i<n; ++i)
{
     DrawScene(i);
     Wait();
}


一、双缓冲技术
在计算机上的动画与实际的动画有些不一样:实际的动画都是先画好了,播放的时候直接拿出来显示就行。计算机动画则是画一张,就拿出来一张,再画下一张,再拿出来。若是所须要绘制的图形很简单,那么这样也没什么问题。但一旦图形比较复杂,绘制须要的时间较长,问题就会变得突出。
让咱们把计算机想象成一个画图比较快的人,假如他直接在屏幕上画图,而图形比较复杂,则有可能在他只画了某幅图的一半的时候就被观众看到。然后面虽然他把画补全了,但观众的眼睛却又没有反应过来,还停留在原来那个残缺的画面上。也就是说,有时候观众看到完整的图象,有时却又只看到残缺的图象,这样就形成了屏幕的闪烁。
如何解决这一问题呢?咱们设想有两块画板,画图的人在旁边画,画好之后把他手里的画板与挂在屏幕上的画板相交换。这样以来,观众就不会看到残缺的画了。这一技术被应用到计算机图形中,称为双缓冲技术。即:在存储器(颇有多是显存)中开辟两块区域,一块做为发送到显示器的数据,一块做为绘画的区域,在适当的时候交换它们。因为交换两块内存区域实际上只须要交换两个指针,这一方法效率很是高,因此被普遍的采用。
注意:虽然绝大多数平台都支持双缓冲技术,但这一技术并非OpenGL标准中的内容。OpenGL为了保证更好的可移植性,容许在实现时不使用双缓冲技术。固然,咱们经常使用的PC都是支持双缓冲技术的。
要启动双缓冲功能,最简单的办法就是使用GLUT工具包。咱们之前在main函数里面写:
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
其中GLUT_SINGLE表示单缓冲,若是改为GLUT_DOUBLE就是双缓冲了。
固然还有须要更改的地方——每次绘制完成时,咱们须要交换两个缓冲区,把绘制好的信息用于屏幕显示(不然不管怎么绘制,仍是什么都看不到)。若是使用GLUT工具包,也能够很轻松的完成这一工做,只要在绘制完成时简单的调用glutSwapBuffers函数就能够了。


二、实现连续动画
彷佛没有任何疑问,咱们应该把绘制动画的代码写成下面这个样子:
for(i=0; i<n; ++i)
{
     DrawScene(i);
     glutSwapBuffers();
     Wait();
}
但事实上,这样作不太符合窗口系统的程序设计思路。还记得咱们的第一个OpenGL程序吗?咱们在main函数里写:glutDisplayFunc(&myDisplay);
意思是对系统说:若是你须要绘制窗口了,请调用myDisplay这个函数。为何咱们不直接调用myDisplay,而要采用这种看似“舍近求远”的作法呢?缘由在于——咱们本身的程序没法掌握究竟何时该绘制窗口。由于通常的窗口系统——拿咱们熟悉一点的来讲——Windows和X窗口系统,都是支持同时显示多个窗口的。假如你的程序窗口碰巧被别的窗口遮住了,后来用户又把原来遮住的窗口移开,这时你的窗口须要从新绘制。很不幸的,你没法知道这一事件发生的具体时间。所以这一切只好委托操做系统来办了。
如今咱们再看上面那个循环。既然DrawScene均可以交给操做系统来代办了,那让整个循环运行起来的工做是否也能够交给操做系统呢?答案是确定的。咱们先前的思路是:绘制,而后等待一段时间;再绘制,再等待一段时间。但若是去掉等待的时间,就变成了绘制,绘制,……,不停的绘制。——固然了,资源是公用的嘛,杀毒软件总要工做吧?个人下载不能停下来吧?个人mp3播放还不能给耽搁了。总不能由于咱们的动画,让其余的工做都停下来。所以,咱们须要在CPU空闲的时间绘制。
这里的“在CPU空闲的时间绘制”和咱们在第一课讲的“在须要绘制的时候绘制”有些共通,都是“在XX时间作XX事”,GLUT工具包也提供了一个比较相似的函数:glutIdleFunc,表示在CPU空闲的时间调用某一函数。其实GLUT还提供了一些别的函数,例如“在键盘按下时作某事”等。

到如今,咱们已经能够初步开始制做动画了。好的,就拿上次那个“太阳、地球和月亮”的程序开刀,让地球和月亮本身动起来。

Code:


#include <GL/glut.h>

// 太阳、地球和月亮
// 假设每月都是30天
// 一年12个月,共是360天
static int day = 200; // day的变化:从0到359
void myDisplay(void)
{
     /****************************************************
      这里的内容照搬上一课的,只由于使用了双缓冲,补上最后这句
     *****************************************************/
     glutSwapBuffers();
}

void myIdle(void)
{
     /* 新的函数,在空闲时调用,做用是把日期日后移动一天并从新绘制,达到动画效果 */
     ++day;
     if( day >= 360 )
         day = 0;
     myDisplay();
}

int main(int argc, char *argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 修改了参数为GLUT_DOUBLE
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(400, 400);
     glutCreateWindow("太阳,地球和月亮");    // 改了窗口标题
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);                // 新加入了这句
     glutMainLoop();
     return 0;
}


三、关于垂直同步
代码是写好了,但相信你们还有疑问。某些朋友可能在运行时发现,虽然CPU几乎都用上了,但运动速度很快,根本看不清楚,另外一些朋友在运行时发现CPU使用率很低,根本就没有把空闲时间彻底利用起来。但对于上面那段代码来讲,这些现象都是合理的。这里就牵涉到关于垂直同步的问题。

你们知道显示器的刷新率是比较有限的,通常为60~120Hz,也就是一秒钟刷新60~120次。但若是叫计算机绘制一个简单的画面,例如只有一个三角形,则一秒钟能够绘制成千上万次。所以,若是最大限度的利用计算机的处理能力,绘制不少幅画面,但显示器的刷新速度却跟不上,这不只形成性能的浪费,还可能带来一些负面影响(例如,显示器只刷新到一半时,须要绘制的内容却变化了,因为显示器是逐行刷新的,因而显示器上半部分和下半部分其实是来自两幅画面)。采用垂直同步技术能够解决这一问题。即,只有在显示器刷新时,才把绘制好的图象传输出去供显示。这样一来,计算机就没必要去绘制大量的根本就用不到的图象了。若是显示器的刷新率为85Hz,则计算机一秒钟只须要绘制85幅图象就足够,若是场景足够简单,就会形成比较多的CPU空闲。
几乎全部的显卡都支持“垂直同步”这一功能。
垂直同步也有它的问题。若是刷新频率为60Hz,则在绘制比较简单的场景时,绘制一幅图画须要的时间很段,帧速能够恒定在60FPS(即60帧/秒)。若是场景变得复杂,绘制一幅图画的时间超过了1/60秒,则帧速将急剧降低。
若是绘制一幅图画的时间为1/50,则在第一个1/60秒时,显示器须要刷新了,但因为新的图画没有画好,因此只能显示原来的图画,等到下一个1/60秒时才显示新的图画。因而显示一幅图画实际上用了1/30秒,帧速为30FPS。(若是不采用垂直同步,则帧速应该是50FPS)
若是绘制一幅图画的时间更长,则降低的趋势就是阶梯状的:60FPS,30FPS,20FPS,……(60/1,60/2,60/3,……)
若是每一幅图画的复杂程度是不一致的,且绘制它们须要的时间都在1/60上下。则在1/60时间内画完时,帧速为60FPS,在1/60时间未完成时,帧速为30FPS,这就形成了帧速的跳动。这是很麻烦的事情,须要避免它——要么想办法简化每一画面的绘制时间,要么都延迟一小段时间,以做到统一。

回过头来看前面的问题。若是使用了大量的CPU并且速度很快没法看清,则打开垂直同步能够解决该问题。固然若是你认为垂直同步有这样那样的缺点,也能够关闭它。——至于如何打开和关闭,因操做系统而异了。具体步骤请本身搜索之。

固然,也有其它办法能够控制动画的帧速,或者尽可能让动画的速度尽可能和帧速无关。不过这里面不少内容都是与操做系统比较紧密的,何况它们跟OpenGL关系也不太大。这里就不作介绍了。


四、计算帧速
不知道你们玩过3D Mark这个软件没有,它能够运行各类场景,测出帧速,而且为你的系统给出评分。这里我也介绍一个计算帧速的方法。
根据定义,帧速就是一秒钟内播放的画面数目(FPS)。咱们能够先测量绘制两幅画面之间时间t,而后求它的倒数便可。假如t=0.05s,则FPS的值就是1/0.05=20。
理论上是如此了,但是如何获得这个时间呢?一般C语言的time函数精确度通常只到一秒,确定是不行了。clock函数也就到十毫秒左右,仍是有点不够。由于FPS为60和FPS为100的时候,t的值都是十几毫秒。
你知道如何测量一张纸的厚度吗?一个粗略的办法就是:用不少张纸叠在一块儿测厚度,计算平均值就能够了。咱们这里也能够这样办。测量绘制50幅画面(包括垂直同步等因素的等待时间)须要的时间t',由t'=t*50很容易的获得FPS=1/t=50/t'
下面这段代码能够统计该函数自身的调用频率,(原理就像上面说的那样),程序并不复杂,而且这并不属于OpenGL的内容,因此我不打算详细讲述它。

Code:

#include <time.h>
double CalFrequency()
{
     static int count;
     static double save;
     static clock_t last, current;
     double timegap;

     ++count;
     if( count <= 50 )
         return save;
     count = 0;
     last = current;
     current = clock();
     timegap = (current-last)/(double)CLK_TCK;
     save = 50.0/timegap;
     return save;
}



最后,要把计算的帧速显示出来,但咱们并无学习如何使用OpenGL把文字显示到屏幕上。——但不要忘了,在咱们的图形窗口背后,还有一个命令行窗口~使用printf函数就能够轻易的输出文字了。
#include <stdio.h>

double FPS = CalFrequency();
printf("FPS = %f\n", FPS);
最后的一步,也被咱们解决了——虽然作法不太雅观,不要紧,之后咱们还会改善它的。


时间过得过久,每次给的程序都只是一小段,一些朋友不免会出问题。
如今,我给出一个比较完整的程序,供你们参考。

Code:

#include <GL/glut.h>
#include <stdio.h>
#include <time.h>

// 太阳、地球和月亮
// 假设每月都是12天
// 一年12个月,共是360天
static int day = 200; // day的变化:从0到359

double CalFrequency()
{
     static int count;
     static double save;
     static clock_t last, current;
     double timegap;

     ++count;
     if( count <= 50 )
         return save;
     count = 0;
     last = current;
     current = clock();
     timegap = (current-last)/(double)CLK_TCK;
     save = 50.0/timegap;
     return save;
}

void myDisplay(void)
{
     double FPS = CalFrequency();
     printf("FPS = %f\n", FPS);

     glEnable(GL_DEPTH_TEST);
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(75, 1, 1, 400000000);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1);

     // 绘制红色的“太阳”
     glColor3f(1.0f, 0.0f, 0.0f);
     glutSolidSphere(69600000, 20, 20);
     // 绘制蓝色的“地球”
     glColor3f(0.0f, 0.0f, 1.0f);
     glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(150000000, 0.0f, 0.0f);
     glutSolidSphere(15945000, 20, 20);
     // 绘制黄色的“月亮”
     glColor3f(1.0f, 1.0f, 0.0f);
     glRotatef(day/30.0*360.0 - day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(38000000, 0.0f, 0.0f);
     glutSolidSphere(4345000, 20, 20);

     glFlush();
     glutSwapBuffers();
}

void myIdle(void)
{
     ++day;
     if( day >= 360 )
         day = 0;
     myDisplay();
}

int main(int argc, char *argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(400, 400);
     glutCreateWindow("太阳,地球和月亮");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}



小结:
OpenGL动画和传统意义上的动画类似,都是把画面一幅一幅的呈如今观众面前。一旦画面变换的速度快了,观众就会认为画面是连续的。
双缓冲技术是一种在计算机图形中广泛采用的技术,绝大多数OpenGL实现都支持双缓冲技术。
一般都是利用CPU空闲的时候绘制动画,但也能够有其它的选择。
介绍了垂直同步的相关知识。
介绍了一种简单的计算帧速(FPS)的方法。
最后,咱们列出了一份完整的天体动画程序清单。

=====================    第六课 完    =====================
=====================TO BE CONTINUED=====================




OpenGL入门学习[七]


今天要讲的是OpenGL光照的基本知识。虽然内容显得有点多,但条理还算比较清晰,理解起来应该没有困难。即便对于一些内容没有记住,问题也不大——光照部分是一个比较独立的内容,它的学习与其它方面的学习能够分开,不像视图变换那样,影响到许多方面。课程的最后给出了一个有关光照效果的动画演示程序,我想你们会喜欢的。
从生理学的角度上讲,眼睛之因此看见各类物体,是由于光线直接或间接的从它们那里到达了眼睛。人类对于光线强弱的变化的反应,比对于颜色变化的反应来得灵敏。所以对于人类而言,光线很大程度上表现了物体的立体感。
请看图1,图中绘制了两个大小相同的白色球体。其中右边的一个是没有使用任何光照效果的,它看起来就像是一个二维的圆盘,没有立体的感受。左边的一个是使用了简单的光照效果的,咱们经过光照的层次,很容易的认为它是一个三维的物体。
http://blog.programfan.com/upfile/200702/2007022315149.jpg
图1

OpenGL对于光照效果提供了直接的支持,只须要调用某些函数,即可以实现简单的光照效果。可是在这以前,咱们有必要了解一些基础知识。
1、创建光照模型
在现实生活中,某些物体自己就会发光,例如太阳、电灯等,而其它物体虽然不会发光,但能够反射来自其它物体的光。这些光经过各类方式传播,最后进入咱们的眼睛——因而一幅画面就在咱们的眼中造成了。
就目前的计算机而言,要准确模拟各类光线的传播,这是没法作到的事情。好比一个四面都是粗糙墙壁的房间,一盏电灯所发出的光线在很短的时间内就会通过很是屡次的反射,最终几乎布满了房间的每个角落,这一过程即便使用目前运算速度最快的计算机,也没法精确模拟。不过,咱们并不须要精确的模拟各类光线,只须要找到一种近似的计算方式,使它的最终结果让咱们的眼睛认为它是真实的,这就能够了。
OpenGL在处理光照时采用这样一种近似:把光照系统分为三部分,分别是光源、材质和光照环境。光源就是光的来源,能够是前面所说的太阳或者电灯等。材质是指接受光照的各类物体的表面,因为物体如何反射光线只由物体表面决定(OpenGL中没有考虑光的折射),材质特色就决定了物体反射光线的特色。光照环境是指一些额外的参数,它们将影响最终的光照画面,好比一些光线通过屡次反射后,已经没法分清它到底是由哪一个光源发出,这时,指定一个“环境亮度”参数,能够使最后造成的画面更接近于真实状况。
在物理学中,光线若是射入理想的光滑平面,则反射后的光线是很规则的(这样的反射称为镜面反射)。光线若是射入粗糙的、不光滑的平面,则反射后的光线是杂乱的(这样的反射称为漫反射)。现实生活中的物体在反射光线时,并非绝对的镜面反射或漫反射,但能够当作是这两种反射的叠加。对于光源发出的光线,能够分别设置其通过镜面反射和漫反射后的光线强度。对于被光线照射的材质,也能够分别设置光线通过镜面反射和漫反射后的光线强度。这些因素综合起来,就造成了最终的光照效果。

2、法线向量
根据光的反射定律,由光的入射方向和入射点的法线就能够获得光的出射方向。所以,对于指定的物体,在指定了光源后,便可计算出光的反射方向,进而计算出光照效果的画面。在OpenGL中,法线的方向是用一个向量来表示。
不幸的是,OpenGL并不会根据你所指定的多边形各个顶点来计算出这些多边形所构成的物体的表面的每一个点的法线(这话听着有些迷糊),一般,为了实现光照效果,须要在代码中为每个顶点指定其法线向量。
指定法线向量的方式与指定颜色的方式有雷同之处。在指定颜色时,只须要指定每个顶点的颜色,OpenGL就能够自行计算顶点之间的其它点的颜色。而且,颜色一旦被指定,除非再指定新的颜色,不然之后指定的全部顶点都将以这一贯量做为本身的颜色。在指定法线向量时,只须要指定每个顶点的法线向量,OpenGL会自行计算顶点之间的其它点的法线向量。而且,法线向量一旦被指定,除非再指定新的法线向量,不然之后指定的全部顶点都将以这一贯量做为本身的法线向量。使用glColor*函数能够指定颜色,而使用glNormal*函数则能够指定法线向量。
注意:使用glTranslate*函数或者glRotate*函数能够改变物体的外观,但法线向量并不会随之改变。然而,使用glScale*函数,对每一坐标轴进行不一样程度的缩放,颇有可能致使法线向量的不正确,虽然OpenGL提供了一些措施来修正这一问题,但由此也带来了各类开销。所以,在使用了法线向量的场合,应尽可能避免使用glScale*函数。即便使用,也最好保证各坐标轴进行等比例缩放。
3、控制光源
在OpenGL中,仅仅支持有限数量的光源。使用GL_LIGHT0表示第0号光源,GL_LIGHT1表示第1号光源,依次类推,OpenGL至少会支持8个光源,即GL_LIGHT0到GL_LIGHT7。使用glEnable函数能够开启它们。例如,glEnable(GL_LIGHT0);能够开启第0号光源。使用glDisable函数则能够关闭光源。一些OpenGL实现可能支持更多数量的光源,但总的来讲,开启过多的光源将会致使程序运行速度的严重降低,玩过3D Mark的朋友可能多少也有些体会。一些场景中可能有成百上千的电灯,这时可能须要采起一些近似的手段来进行编程,不然以目前的计算机而言,是没法运行这样的程序的。
每个光源均可以设置其属性,这一动做是经过glLight*函数完成的。glLight*函数具备三个参数,第一个参数指明是设置哪个光源的属性,第二个参数指明是设置该光源的哪个属性,第三个参数则是指明把该属性值设置成多少。光源的属性众多,下面将分别介绍。
(1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR属性。这三个属性表示了光源所发出的光的反射特性(以及颜色)。每一个属性由四个值表示,分别表明了颜色的R, G, B, A值。GL_AMBIENT表示该光源所发出的光,通过很是屡次的反射后,最终遗留在整个光照环境中的强度(颜色)。GL_DIFFUSE表示该光源所发出的光,照射到粗糙表面时通过漫反射,所获得的光的强度(颜色)。GL_SPECULAR表示该光源所发出的光,照射到光滑表面时通过镜面反射,所获得的光的强度(颜色)。
(2)GL_POSITION属性。表示光源所在的位置。由四个值(X, Y, Z, W)表示。若是第四个值W为零,则表示该光源位于无限远处,前三个值表示了它所在的方向。这种光源称为方向性光源,一般,太阳能够近似的被认为是方向性光源。若是第四个值W不为零,则X/W, Y/W, Z/W表示了光源的位置。这种光源称为位置性光源。对于位置性光源,设置其位置与设置多边形顶点的方式类似,各类矩阵变换函数例如:glTranslate*、glRotate*等在这里也一样有效。方向性光源在计算时比位置性光源快了很多,所以,在视觉效果容许的状况下,应该尽量的使用方向性光源。
(3)GL_SPOT_DIRECTION、GL_SPOT_EXPONENT、GL_SPOT_CUTOFF属性。表示将光源做为聚光灯使用(这些属性只对位置性光源有效)。不少光源都是向四面八方发射光线,但有时候一些光源则是只向某个方向发射,好比手电筒,只向一个较小的角度发射光线。GL_SPOT_DIRECTION属性有三个值,表示一个向量,即光源发射的方向。GL_SPOT_EXPONENT属性只有一个值,表示聚光的程度,为零时表示光照范围内向各方向发射的光线强度相同,为正数时表示光照向中央集中,正对发射方向的位置受到更多光照,其它位置受到较少光照。数值越大,聚光效果就越明显。GL_SPOT_CUTOFF属性也只有一个值,表示一个角度,它是光源发射光线所覆盖角度的一半(见图2),其取值范围在0到90之间,也能够取180这个特殊值。取值为180时表示光源发射光线覆盖360度,即不使用聚光灯,向全周围发射。
http://blog.programfan.com/upfile/200702/20070223151415.gif
图2

(4)GL_CONSTANT_ATTENUATION、GL_LINEAR_ATTENUATION、GL_QUADRATIC_ATTENUATION属性。这三个属性表示了光源所发出的光线的直线传播特性(这些属性只对位置性光源有效)。现实生活中,光线的强度随着距离的增长而减弱,OpenGL把这个减弱的趋势抽象成函数:
衰减因子 = 1 / (k1 + k2 * d + k3 * k3 * d)
其中d表示距离,光线的初始强度乘以衰减因子,就获得对应距离的光线强度。k1, k2, k3分别就是GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION。经过设置这三个常数,就能够控制光线在传播过程当中的减弱趋势。

属性还真是很多。固然了,若是是使用方向性光源,(3)(4)这两类属性就不会用到了,问题就变得简单明了。
4、控制材质
材质与光源类似,也须要设置众多的属性。不一样的是,光源是经过glLight*函数来设置的,而材质则是经过glMaterial*函数来设置的。
glMaterial*函数有三个参数。第一个参数表示指定哪一面的属性。能够是GL_FRONT、GL_BACK或者GL_FRONT_AND_BACK。分别表示设置“正面”“背面”的材质,或者两面同时设置。(关于“正面”“背面”的内容须要参看前些课程的内容)第2、第三个参数与glLight*函数的第2、三个参数做用相似。下面分别说明glMaterial*函数能够指定的材质属性。
(1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR属性。这三个属性与光源的三个对应属性相似,每一属性都由四个值组成。GL_AMBIENT表示各类光线照射到该材质上,通过不少次反射后最终遗留在环境中的光线强度(颜色)。GL_DIFFUSE表示光线照射到该材质上,通过漫反射后造成的光线强度(颜色)。GL_SPECULAR表示光线照射到该材质上,通过镜面反射后造成的光线强度(颜色)。一般,GL_AMBIENT和GL_DIFFUSE都取相同的值,能够达到比较真实的效果。使用GL_AMBIENT_AND_DIFFUSE能够同时设置GL_AMBIENT和GL_DIFFUSE属性。
(2)GL_SHININESS属性。该属性只有一个值,称为“镜面指数”,取值范围是0到128。该值越小,表示材质越粗糙,点光源发射的光线照射到上面,也能够产生较大的亮点。该值越大,表示材质越相似于镜面,光源照射到上面后,产生较小的亮点。
(3)GL_EMISSION属性。该属性由四个值组成,表示一种颜色。OpenGL认为该材质自己就微微的向外发射光线,以致于眼睛感受到它有这样的颜色,但这光线又比较微弱,以致于不会影响到其它物体的颜色。
(4)GL_COLOR_INDEXES属性。该属性仅在颜色索引模式下使用,因为颜色索引模式下的光照比RGBA模式要复杂,而且使用范围较小,这里不作讨论。
5、选择光照模型
这里所说的“光照模型”是OpenGL的术语,它至关于咱们在前面提到的“光照环境”。在OpenGL中,光照模型包括四个部分的内容:全局环境光线(即那些充分散射,没法分清究竟来自哪一个光源的光线)的强度、观察点位置是在较近位置仍是在无限远处、物体正面与背面是否分别计算光照、镜面颜色(即GL_SPECULAR属性所指定的颜色)的计算是否从其它光照计算中分离出来,并在纹理操做之后在进行应用。
以上四方面的内容都经过同一个函数glLightModel*来进行设置。该函数有两个参数,第一个表示要设置的项目,第二个参数表示要设置成的值。
GL_LIGHT_MODEL_AMBIENT表示全局环境光线强度,由四个值组成。
GL_LIGHT_MODEL_LOCAL_VIEWER表示是否在近处观看,如果则设置为GL_TRUE,不然(即在无限远处观看)设置为GL_FALSE。
GL_LIGHT_MODEL_TWO_SIDE表示是否执行双面光照计算。若是设置为GL_TRUE,则OpenGL不只将根据法线向量计算正面的光照,也会将法线向量反转并计算背面的光照。
GL_LIGHT_MODEL_COLOR_CONTROL表示颜色计算方式。若是设置为GL_SINGLE_COLOR,表示按一般顺序操做,先计算光照,再计算纹理。若是设置为GL_SEPARATE_SPECULAR_COLOR,表示将GL_SPECULAR属性分离出来,先计算光照的其它部分,待纹理操做完成后再计算GL_SPECULAR。后者一般能够使画面效果更为逼真(固然,若是自己就没有执行任何纹理操做,这样的分离就没有任何意义)。

6、最后的准备
到如今能够说是完事俱备了。不过,OpenGL默认是关闭光照处理的。要打开光照处理功能,使用下面的语句:
glEnable(GL_LIGHTING);
要关闭光照处理功能,使用glDisable(GL_LIGHTING);便可。
7、示例程序
到如今,咱们已经能够编写简单的使用光照的OpenGL程序了。
咱们仍然以太阳、地球做为例子(此次就不考虑月亮了^-^),把太阳做为光源,模拟地球围绕太阳转动时光照的变化。因而,须要设置一个光源——太阳,设置两种材质——太阳的材质和地球的材质。把太阳光线设置为白色,位置在画面正中。把太阳的材质设置为微微散发出红色的光芒,把地球的材质设置为微微散发出暗淡的蓝色光芒,而且反射蓝色的光芒,镜面指数设置成一个比较小的值。简单起见,再也不考虑太阳和地球的大小关系,用一样大小的球体来代替之。
关于法线向量。球体表面任何一点的法线向量,就是球心到该点的向量。若是使用glutSolidSphere函数来绘制球体,则该函数会自动的指定这些法线向量,没必要再手工指出。若是是本身指定若干的顶点来绘制一个球体,则须要本身指定法线响亮。
因为咱们使用的太阳是一个位置性光源,在设置它的位置时,须要利用到矩阵变换。所以,在设置光源的位置之前,须要先设置好各类矩阵。利用gluPerspective函数来建立具备透视效果的视图。咱们也将利用前面课程所学习的动画知识,让整个画面动起来。

下面给出具体的代码:
#include <gl/glut.h>

#define WIDTH 400
#define HEIGHT 400

static GLfloat angle = 0.0f;

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 建立透视效果视图
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(90.0f, 1.0f, 1.0f, 20.0f);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0.0, 5.0, -10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

     // 定义太阳光源,它是一种白色的光源
     {
     GLfloat sun_light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
     GLfloat sun_light_ambient[]   = {0.0f, 0.0f, 0.0f, 1.0f};
     GLfloat sun_light_diffuse[]   = {1.0f, 1.0f, 1.0f, 1.0f};
     GLfloat sun_light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

     glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position);
     glLightfv(GL_LIGHT0, GL_AMBIENT,   sun_light_ambient);
     glLightfv(GL_LIGHT0, GL_DIFFUSE,   sun_light_diffuse);
     glLightfv(GL_LIGHT0, GL_SPECULAR, sun_light_specular);

     glEnable(GL_LIGHT0);
     glEnable(GL_LIGHTING);
     glEnable(GL_DEPTH_TEST);
     }

     // 定义太阳的材质并绘制太阳
     {
         GLfloat sun_mat_ambient[]   = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_diffuse[]   = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_emission[] = {0.5f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_shininess   = 0.0f;

         glMaterialfv(GL_FRONT, GL_AMBIENT,    sun_mat_ambient);
         glMaterialfv(GL_FRONT, GL_DIFFUSE,    sun_mat_diffuse);
         glMaterialfv(GL_FRONT, GL_SPECULAR,   sun_mat_specular);
         glMaterialfv(GL_FRONT, GL_EMISSION,   sun_mat_emission);
         glMaterialf (GL_FRONT, GL_SHININESS, sun_mat_shininess);

         glutSolidSphere(2.0, 40, 32);
     }

     // 定义地球的材质并绘制地球
     {
         GLfloat earth_mat_ambient[]   = {0.0f, 0.0f, 0.5f, 1.0f};
         GLfloat earth_mat_diffuse[]   = {0.0f, 0.0f, 0.5f, 1.0f};
         GLfloat earth_mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
         GLfloat earth_mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat earth_mat_shininess   = 30.0f;

         glMaterialfv(GL_FRONT, GL_AMBIENT,    earth_mat_ambient);
         glMaterialfv(GL_FRONT, GL_DIFFUSE,    earth_mat_diffuse);
         glMaterialfv(GL_FRONT, GL_SPECULAR,   earth_mat_specular);
         glMaterialfv(GL_FRONT, GL_EMISSION,   earth_mat_emission);
         glMaterialf (GL_FRONT, GL_SHININESS, earth_mat_shininess);

         glRotatef(angle, 0.0f, -1.0f, 0.0f);
         glTranslatef(5.0f, 0.0f, 0.0f);
         glutSolidSphere(2.0, 40, 32);
     }

     glutSwapBuffers();
}
void myIdle(void)
{
     angle += 1.0f;
     if( angle >= 360.0f )
         angle = 0.0f;
     myDisplay();
}

int main(int argc, char* argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
     glutInitWindowPosition(200, 200);
     glutInitWindowSize(WIDTH, HEIGHT);
     glutCreateWindow("OpenGL光照演示");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}
小结:
本课介绍了OpenGL光照的基本知识。OpenGL把光照分解为光源、材质、光照模式三个部分,根据这三个部分的各类信息,以及物体表面的法线向量,能够计算获得最终的光照效果。
光源、材质和光照模式都有各自的属性,尽管属性种类繁多,但这些属性都只用不多的几个函数来设置。使用glLight*函数可设置光源的属性,使用glMaterial*函数可设置材质的属性,使用glLightModel*函数可设置光照模式。
GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR这三种属性是光源和材质所共有的,若是某光源发出的光线照射到某材质的表面,则最终的漫反射强度由两个GL_DIFFUSE属性共同决定,最终的镜面反射强度由两个GL_SPECULAR属性共同决定。
能够使用多个光源来实现各类逼真的效果,然而,光源数量的增长将形成程序运行速度的明显降低。
在使用OpenGL光照过程当中,属性的种类和数量都很是繁多,一般,须要不少的经验才能够熟练的设置各类属性,从而造成逼真的光照效果。(各位也看到了,其实这个课程的示例程序中,属性设置也不怎么好)。然而,设置这些属性的艺术性远远超过了技术性,每每是一些美术制做人员设置好各类属性(并保存为文件),而后由程序员编写的程序去执行绘制工做。所以,即便目前没法熟练运用各类属性,也没必要过于担忧。若是条件容许,能够玩玩相似3DS MAX之类的软件,对理解光照、熟悉各类属性设置会有一些帮助。
在课程的最后,咱们给出了一个样例程序,演示了太阳和地球模型中的光照效果。




OpenGL入门学习[八]


今天介绍关于OpenGL显示列表的知识。本课内容并很少,但须要一些理解能力。在学习时,能够将显示列表与C语言的“函数”进行类比,加深体会。

咱们已经知道,使用OpenGL其实只要调用一系列的OpenGL函数就能够了。然而,这种方式在一些时候可能致使问题。好比某个画面中,使用了数千个多边形来表现一个比较真实的人物,OpenGL为了产生这数千个多边形,就须要不停的调用glVertex*函数,每个多边形将至少调用三次(由于多边形至少有三个顶点),因而绘制一个比较真实的人物就须要调用上万次的glVertex*函数。更糟糕的是,若是咱们须要每秒钟绘制60幅画面,则每秒调用的glVertex*函数次数就会超过数十万次,乃至接近百万次。这样的状况是咱们所不肯意看到的。
同时,考虑这样一段代码:

const int segments = 100;
const GLfloat pi = 3.14f;
int i;
glLineWidth(10.0);
glBegin(GL_LINE_LOOP);
for(i=0; i<segments; ++i)
{
     GLfloat tmp = 2 * pi * i / segments;
     glVertex2f(cos(tmp), sin(tmp));
}
glEnd();


这段代码将绘制一个圆环。若是咱们在每次绘制图象时调用这段代码,则虽然能够达到绘制圆环的目的,可是cos、sin等开销较大的函数被屡次调用,浪费了CPU资源。若是每个顶点不是经过cos、sin等函数获得,而是使用更复杂的运算方式来获得,则浪费的现象就更加明显。

通过分析,咱们能够发现上述两个问题的共同点:程序屡次执行了重复的工做,致使CPU资源浪费和运行速度的降低。使用显示列表能够较好的解决上述两个问题。
在编写程序时,遇到重复的工做,咱们每每是将重复的工做编写为函数,在须要的地方调用它。相似的,在编写OpenGL程序时,遇到重复的工做,能够建立一个显示列表,把重复的工做装入其中,并在须要的地方调用这个显示列表。
使用显示列表通常有四个步骤:分配显示列表编号、建立显示列表、调用显示列表、销毁显示列表。

1、分配显示列表编号
OpenGL容许多个显示列表同时存在,就好象C语言容许程序中有多个函数同时存在。C语言中,不一样的函数用不一样的名字来区分,而在OpenGL中,不一样的显示列表用不一样的正整数来区分。
你能够本身指定一些各不相同的正整数来表示不一样的显示列表。可是若是你不够当心,可能出现一个显示列表将另外一个显示列表覆盖的状况。为了不这一问题,使用glGenLists函数来自动分配一个没有使用的显示列表编号。
glGenLists函数有一个参数i,表示要分配i个连续的未使用的显示列表编号。返回的是分配的若干连续编号中最小的一个。例如,glGenLists(3);若是返回20,则表示分配了20、2一、22这三个连续的编号。若是函数返回零,表示分配失败。
能够使用glIsList函数判断一个编号是否已经被用做显示列表。

2、建立显示列表
建立显示列表实际上就是把各类OpenGL函数的调用装入到显示列表中。使用glNewList开始装入,使用glEndList结束装入。glNewList有两个参数,第一个参数是一个正整数表示装入到哪一个显示列表。第二个参数有两种取值,若是为GL_COMPILE,则表示如下的内容只是装入到显示列表,但如今不执行它们;若是为GL_COMPILE_AND_EXECUTE,表示在装入的同时,把装入的内容执行一遍。
例如,须要把“设置颜色为红色,而且指定一个坐标为(0, 0)的顶点”这两条命令装入到编号为list的显示列表中,而且在装入的时候不执行,则能够用下面的代码:
glNewList(list, GL_COMPILE);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();

注意:显示列表只能装入OpenGL函数,而不能装入其它内容。例如:
int i = 3;
glNewList(list, GL_COMPILE);
if( i > 20 )
     glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();
其中if这个判断就没有被装入到显示列表。之后即便修改i的值,使i>20的条件成立,则glColor3f这个函数也不会被执行。由于它根本就不存在于显示列表中。

另外,并不是全部的OpenGL函数均可以装入到显示列表中。例如,各类用于查询的函数,它们没法被装入到显示列表,由于它们都具备返回值,而glCallList和glCallLists函数都不知道如何处理这些返回值。在网络方式下,设置客户端状态的函数也没法被装入到显示列表,这是由于显示列表被保存到服务器端,各类设置客户端状态的函数在发送到服务器端之前就被执行了,而服务器端没法执行这些函数。分配、建立、删除显示列表的动做也没法被装入到另外一个显示列表,但调用显示列表的动做则能够被装入到另外一个显示列表。

3、调用显示列表
使用glCallList函数能够调用一个显示列表。该函数有一个参数,表示要调用的显示列表的编号。例如,要调用编号为10的显示列表,直接使用glCallList(10);就能够了。
使用glCallLists函数能够调用一系列的显示列表。该函数有三个参数,第一个参数表示了要调用多少个显示列表。第二个参数表示了这些显示列表的编号的储存格式,能够是GL_BYTE(每一个编号用一个GLbyte表示),GL_UNSIGNED_BYTE(每一个编号用一个GLubyte表示),GL_SHORT,GL_UNSIGNED_SHORT,GL_INT,GL_UNSIGNED_INT,GL_FLOAT。第三个参数表示了这些显示列表的编号所在的位置。在使用该函数前,须要用glListBase函数来设置一个偏移量。假设偏移量为k,且glCallLists中要求调用的显示列表编号依次为l1, l2, l3, ...,则实际调用的显示列表为l1+k, l2+k, l3+k, ...。
例如:
GLuint lists[] = {1, 3, 4, 8};
glListBase(10);
glCallLists(4, GL_UNSIGNED_INT, lists);
则实际上调用的是编号为11, 13, 14, 18的四个显示列表。
注:“调用显示列表”这个动做自己也能够被装在另外一个显示列表中。

4、销毁显示列表
销毁显示列表能够回收资源。使用glDeleteLists来销毁一串编号连续的显示列表。
例如,使用glDeleteLists(20, 4);将销毁20,21,22,23这四个显示列表。
使用显示列表将会带来一些开销,例如,把各类动做保存到显示列表中会占用必定数量的内存资源。但若是使用得当,显示列表能够提高程序的性能。这主要表如今如下方面:
一、明显的减小OpenGL函数的调用次数。若是函数调用是经过网络进行的(Linux等操做系统支持这样的方式,即由应用程序在客户端发出OpenGL请求,由网络上的另外一台服务器进行实际的绘图操做),将显示列表保存在服务器端,能够大大减小网络负担。
二、保存中间结果,避免一些没必要要的计算。例如前面的样例程序中,cos、sin函数的计算结果被直接保存到显示列表中,之后使用时就没必要重复计算。
三、便于优化。咱们已经知道,使用glTranslate*、glRotate*、glScale*等函数时,其实是执行矩阵乘法操做,因为这些函数常常被组合在一块儿使用,一般会出现矩阵的连乘。这时,若是把这些操做保存到显示列表中,则一些复杂的OpenGL版本会尝试先计算出连乘的一部分结果,从而提升程序的运行速度。在其它方面也可能存在相似的例子。
同时,显示列表也为程序的设计带来方便。咱们在设置一些属性时,常常把一些相关的函数放在一块儿调用,(好比,把设置光源的各类属性的函数放到一块儿)这时,若是把这些设置属性的操做装入到显示列表中,则能够实现属性的成组的切换。
固然了,即便使用显示列表在某些状况下能够提升性能,但这种提升极可能并不明显。毕竟,在硬件配置和大体的软件算法都不变的前提下,性能可提高的空间并不大。
显示列表的内容就是这么多了,下面咱们看一个例子。
假设咱们须要绘制一个旋转的彩色正四面体,则能够这样考虑:设置一个全局变量angle,而后让它的值不断的增长(到达360后又恢复为0,周而复始)。每次须要绘制图形时,根据angle的值进行旋转,而后绘制正四面体。这里正四面体采用显示列表来实现,即把绘制正四面体的若干OpenGL函数装到一个显示列表中,而后每次须要绘制时,调用这个显示列表便可。
将正四面体的四个顶点颜色分别设置为红、黄、绿、蓝,经过数学计算,将坐标设置为:
(-0.5, -5*sqrt(5)/48,   sqrt(3)/6),
( 0.5, -5*sqrt(5)/48,   sqrt(3)/6),
(    0, -5*sqrt(5)/48, -sqrt(3)/3),
(    0, 11*sqrt(6)/48,           0)
2007年4月24日修正:以上结果有误,经过计算AB, AC, AD, BC, BD, CD的长度,发现AD, BD, CD的长度与1.0有较大误差。正确的坐标应该是:
    A点:(   0.5,    -sqrt(6)/12, -sqrt(3)/6)
    B点:( -0.5,    -sqrt(6)/12, -sqrt(3)/6)
    C点:(     0,    -sqrt(6)/12,   sqrt(3)/3)
    D点:(     0,     sqrt(6)/4,            0)
    程序代码中也作了相应的修改


下面给出程序代码,你们能够从中体会一下显示列表的用法。

#include <gl/glut.h>

#define WIDTH 400
#define HEIGHT 400

#include <math.h>
#define ColoredVertex(c, v) do{ glColor3fv(c); glVertex3fv(v); }while(0)

GLfloat angle = 0.0f;

void myDisplay(void)
{
     static int list = 0;
     if( list == 0 )
     {
         // 若是显示列表不存在,则建立
         /* GLfloat
             PointA[] = {-0.5, -5*sqrt(5)/48,   sqrt(3)/6},
             PointB[] = { 0.5, -5*sqrt(5)/48,   sqrt(3)/6},
             PointC[] = {    0, -5*sqrt(5)/48, -sqrt(3)/3},
             PointD[] = {    0, 11*sqrt(6)/48,           0}; */

         // 2007年4月27日修改
         GLfloat
             PointA[] = { 0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointB[] = {-0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointC[] = { 0.0f, -sqrt(6.0f)/12,   sqrt(3.0f)/3},
             PointD[] = { 0.0f,    sqrt(6.0f)/4,              0};

         GLfloat
             ColorR[] = {1, 0, 0},
             ColorG[] = {0, 1, 0},
             ColorB[] = {0, 0, 1},
             ColorY[] = {1, 1, 0};

         list = glGenLists(1);
         glNewList(list, GL_COMPILE);
         glBegin(GL_TRIANGLES);
         // 平面ABC
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorB, PointC);
         // 平面ACD
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorY, PointD);
         // 平面CBD
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorY, PointD);
         // 平面BAD
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorY, PointD);
         glEnd();
         glEndList();

         glEnable(GL_DEPTH_TEST);
     }
     // 已经建立了显示列表,在每次绘制正四面体时将调用它
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glPushMatrix();
     glRotatef(angle, 1, 0.5, 0);
     glCallList(list);
     glPopMatrix();
     glutSwapBuffers();
}

void myIdle(void)
{
     ++angle;
     if( angle >= 360.0f )
         angle = 0.0f;
     myDisplay();
}

int main(int argc, char* argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
     glutInitWindowPosition(200, 200);
     glutInitWindowSize(WIDTH, HEIGHT);
     glutCreateWindow("OpenGL 窗口");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}

在程序中,咱们将绘制正四面体的OpenGL函数装到了一个显示列表中,可是,关于旋转的操做却在显示列表以外进行。这是由于若是把旋转的操做也装入到显示列表,则每次旋转的角度都是同样的,不会随着angle的值的变化而变化,因而就不能表现出动态的旋转效果了。
程序运行时,可能感受到画面的立体感不足,这主要是由于没有使用光照的缘故。若是将glColor3fv函数去掉,改成设置各类材质,而后开启光照效果,则能够产生更好的立体感。你们能够本身试着使用光照效果,惟一须要注意的地方就是法线向量的计算。因为这里的正四面体四个顶点坐标选取得比较特殊,使得正四面体的中心坐标正好是(0, 0, 0),所以,每三个顶点坐标的平均值正好就是这三个顶点所组成的平面的法线向量的值。

void setNormal(GLfloat* Point1, GLfloat* Point2, GLfloat* Point3)
{
     GLfloat normal[3];
     int i;
     for(i=0; i<3; ++i)
         normal[i] = (Point1[i]+Point2[i]+Point3[i]) / 3;
     glNormal3fv(normal);
}


限于篇幅,这里就不给出完整的程序了。不过,你们能够自行尝试,看看使用光照后效果有何种改观。尤为是注意四面体各个表面交界的位置,在未使用光照前,几乎看不清轮廓,在使用光照后,可比较容易的区分各个平面,所以立体感获得增强。(见图1,图2)固然了,这样的效果还不够。若是在各表面的交界处设置不少细小的平面,进行平滑处理,则光照后的效果将更真实。但这已经远离本课的内容了。
http://blog.programfan.com/upfile/200703/20070303005337.jpg图一
http://blog.programfan.com/upfile/200703/20070303005342.jpg图二
小结
本课介绍了显示列表的知识和简单的应用。
能够把各类OpenGL函数调用的动做装到显示列表中,之后调用显示列表,就至关于调用了其中的OpenGL函数。显示列表中除了存放对OpenGL函数的调用外,不会存放其它内容。
使用显示列表的过程是:分配一个未使用的显示列表编号,把OpenGL函数调用装入显示列表,调用显示列表,销毁显示列表。
使用显示列表有可能带来程序运行速度的提高,可是这种提高并不必定会很明显。显示列表自己也存在必定的开销。
把绘制固定的物体的OpenGL函数放到一个显示列表中,是一种不错的编程思路。本课最后的例子中使用了这种思路。



OpenGL入门学习[九]


今天介绍关于OpenGL混合的基本知识。混合是一种经常使用的技巧,一般能够用来实现半透明。但其实它也是十分灵活的,你能够经过不一样的设置获得不一样的混合结果,产生一些有趣或者奇怪的图象。
混合是什么呢?混合就是把两种颜色混在一块儿。具体一点,就是把某一像素位置原来的颜色和将要画上去的颜色,经过某种方式混在一块儿,从而实现特殊的效果。
假设咱们须要绘制这样一个场景:透过红色的玻璃去看绿色的物体,那么能够先绘制绿色的物体,再绘制红色玻璃。在绘制红色玻璃的时候,利用“混合”功能,把将要绘制上去的红色和原来的绿色进行混合,因而获得一种新的颜色,看上去就好像玻璃是半透明的。
要使用OpenGL的混合功能,只须要调用:glEnable(GL_BLEND);便可。
要关闭OpenGL的混合功能,只须要调用:glDisable(GL_BLEND);便可。
注意:只有在RGBA模式下,才能够使用混合功能,颜色索引模式下是没法使用混合功能的。
1、源因子和目标因子
前面咱们已经提到,混合须要把原来的颜色和将要画上去的颜色找出来,通过某种方式处理后获得一种新的颜色。这里把将要画上去的颜色称为“源颜色”,把原来的颜色称为“目标颜色”。
OpenGL会把源颜色和目标颜色各自取出,并乘以一个系数(源颜色乘以的系数称为“源因子”,目标颜色乘以的系数称为“目标因子”),而后相加,这样就获得了新的颜色。(也能够不是相加,新版本的OpenGL能够设置运算方式,包括加、减、取二者中较大的、取二者中较小的、逻辑运算等,但咱们这里为了简单起见,不讨论这个了)
下面用数学公式来表达一下这个运算方式。假设源颜色的四个份量(指红色,绿色,蓝色,alpha值)是(Rs, Gs, Bs, As),目标颜色的四个份量是(Rd, Gd, Bd, Ad),又设源因子为(Sr, Sg, Sb, Sa),目标因子为(Dr, Dg, Db, Da)。则混合产生的新颜色能够表示为:
(Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)
固然了,若是颜色的某一份量超过了1.0,则它会被自动截取为1.0,不须要考虑越界的问题。

源因子和目标因子是能够经过glBlendFunc函数来进行设置的。glBlendFunc有两个参数,前者表示源因子,后者表示目标因子。这两个参数能够是多种值,下面介绍比较经常使用的几种。
GL_ZERO:      表示使用0.0做为因子,实际上至关于不使用这种颜色参与混合运算。
GL_ONE:       表示使用1.0做为因子,实际上至关于彻底的使用了这种颜色参与混合运算。
GL_SRC_ALPHA:表示使用源颜色的alpha值来做为因子。
GL_DST_ALPHA:表示使用目标颜色的alpha值来做为因子。
GL_ONE_MINUS_SRC_ALPHA:表示用1.0减去源颜色的alpha值来做为因子。
GL_ONE_MINUS_DST_ALPHA:表示用1.0减去目标颜色的alpha值来做为因子。
除此之外,还有GL_SRC_COLOR(把源颜色的四个份量分别做为因子的四个份量)、GL_ONE_MINUS_SRC_COLOR、GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前两个在OpenGL旧版本中只能用于设置目标因子,后两个在OpenGL旧版本中只能用于设置源因子。新版本的OpenGL则没有这个限制,而且支持新的GL_CONST_COLOR(设定一种常数颜色,将其四个份量分别做为因子的四个份量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、GL_ONE_MINUS_CONST_ALPHA。另外还有GL_SRC_ALPHA_SATURATE。新版本的OpenGL还容许颜色的alpha值和RGB值采用不一样的混合因子。但这些都不是咱们如今所须要了解的。毕竟这仍是入门教材,不须要整得太复杂~

举例来讲:
若是设置了glBlendFunc(GL_ONE, GL_ZERO);,则表示彻底使用源颜色,彻底不使用目标颜色,所以画面效果和不使用混合的时候一致(固然效率可能会低一点点)。若是没有设置源因子和目标因子,则默认状况就是这样的设置。
若是设置了glBlendFunc(GL_ZERO, GL_ONE);,则表示彻底不使用源颜色,所以不管你想画什么,最后都不会被画上去了。(但这并非说这样设置就没有用,有些时候可能有特殊用途)
若是设置了glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);,则表示源颜色乘以自身的alpha值,目标颜色乘以1.0减去源颜色的alpha值,这样一来,源颜色的alpha值越大,则产生的新颜色中源颜色所占比例就越大,而目标颜色所占比例则减少。这种状况下,咱们能够简单的将源颜色的alpha值理解为“不透明度”。这也是混合时最经常使用的方式。
若是设置了glBlendFunc(GL_ONE, GL_ONE);,则表示彻底使用源颜色和目标颜色,最终的颜色实际上就是两种颜色的简单相加。例如红色(1, 0, 0)和绿色(0, 1, 0)相加获得(1, 1, 0),结果为黄色。
注意:
所谓源颜色和目标颜色,是跟绘制的顺序有关的。假如先绘制了一个红色的物体,再在其上绘制绿色的物体。则绿色是源颜色,红色是目标颜色。若是顺序反过来,则红色就是源颜色,绿色才是目标颜色。在绘制时,应该注意顺序,使得绘制的源颜色与设置的源因子对应,目标颜色与设置的目标因子对应。不要被混乱的顺序搞晕了。
2、二维图形混合举例
下面看一个简单的例子,实现将两种不一样的颜色混合在一块儿。为了便于观察,咱们绘制两个矩形:glRectf(-1, -1, 0.5, 0.5);glRectf(-0.5, -0.5, 1, 1);,这两个矩形有一个重叠的区域,便于咱们观察混合的效果。
先来看看使用glBlendFunc(GL_ONE, GL_ZERO);的,它的结果与不使用混合时相同。

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);

     glEnable(GL_BLEND);
     glBlendFunc(GL_ONE, GL_ZERO);

     glColor4f(1, 0, 0, 0.5);
     glRectf(-1, -1, 0.5, 0.5);
     glColor4f(0, 1, 0, 0.5);
     glRectf(-0.5, -0.5, 1, 1);

     glutSwapBuffers();
}


尝试把glBlendFunc的参数修改成glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);以及glBlendFunc(GL_ONE, GL_ONE);,观察效果。第一种状况下,效果与没有使用混合时相同,后绘制的图形会覆盖先绘制的图形。第二种状况下,alpha被看成“不透明度”,因为被设置为0.5,因此两个矩形看上去都是半透明的,乃至于看到黑色背景。第三种是将颜色相加,红色和绿色相加获得黄色。
http://blog.programfan.com/upfile/200704/20070406022726.jpghttp://blog.programfan.com/upfile/200704/20070406022731.jpghttp://blog.programfan.com/upfile/200704/20070406022735.jpg

3、实现三维混合
也许你火烧眉毛的想要绘制一个三维的带有半透明物体的场景了。可是如今恐怕还不行,还有一点是在进行三维场景的混合时必须注意的,那就是深度缓冲。
深度缓冲是这样一段数据,它记录了每个像素距离观察者有多近。在启用深度缓冲测试的状况下,若是将要绘制的像素比原来的像素更近,则像素将被绘制。不然,像素就会被忽略掉,不进行绘制。这在绘制不透明的物体时很是有用——无论是先绘制近的物体再绘制远的物体,仍是先绘制远的物体再绘制近的物体,或者干脆以混乱的顺序进行绘制,最后的显示结果老是近的物体遮住远的物体。
然而在你须要实现半透明效果时,发现一切都不是那么美好了。若是你绘制了一个近距离的半透明物体,则它在深度缓冲区内保留了一些信息,使得远处的物体将没法再被绘制出来。虽然半透明的物体仍然半透明,但透过它看到的却不是正确的内容了。
要解决以上问题,须要在绘制半透明物体时将深度缓冲区设置为只读,这样一来,虽然半透明物体被绘制上去了,深度缓冲区还保持在原来的状态。若是再有一个物体出如今半透明物体以后,在不透明物体以前,则它也能够被绘制(由于此时深度缓冲区中记录的是那个不透明物体的深度)。之后再要绘制不透明物体时,只须要再将深度缓冲区设置为可读可写的形式便可。嗯?你问我怎么绘制一个一部分半透明一部分不透明的物体?这个好办,只须要把物体分为两个部分,一部分全是半透明的,一部分全是不透明的,分别绘制就能够了。
即便使用了以上技巧,咱们仍然不能为所欲为的按照混乱顺序来进行绘制。必须是先绘制不透明的物体,而后绘制透明的物体。不然,假设背景为蓝色,近处一块红色玻璃,中间一个绿色物体。若是先绘制红色半透明玻璃的话,它先和蓝色背景进行混合,则之后绘制中间的绿色物体时,想单独与红色玻璃混合已经不能实现了。
总结起来,绘制顺序就是:首先绘制全部不透明的物体。若是两个物体都是不透明的,则谁先谁后都没有关系。而后,将深度缓冲区设置为只读。接下来,绘制全部半透明的物体。若是两个物体都是半透明的,则谁先谁后只须要根据本身的意愿(注意了,先绘制的将成为“目标颜色”,后绘制的将成为“源颜色”,因此绘制的顺序将会对结果形成一些影响)。最后,将深度缓冲区设置为可读可写形式。
调用glDepthMask(GL_FALSE);可将深度缓冲区设置为只读形式。调用glDepthMask(GL_TRUE);可将深度缓冲区设置为可读可写形式。
一些网上的教程,包括大名鼎鼎的NeHe教程,都在使用三维混合时直接将深度缓冲区禁用,即调用glDisable(GL_DEPTH_TEST);。这样作并不正确。若是先绘制一个不透明的物体,再在其背后绘制半透明物体,原本后面的半透明物体将不会被显示(被不透明的物体遮住了),但若是禁用深度缓冲,则它仍然将会显示,并进行混合。NeHe提到某些显卡在使用glDepthMask函数时可能存在一些问题,但多是因为个人阅历有限,并无发现这样的状况。

那么,实际的演示一下吧。咱们来绘制一些半透明和不透明的球体。假设有三个球体,一个红色不透明的,一个绿色半透明的,一个蓝色半透明的。红色最远,绿色在中间,蓝色最近。根据前面所讲述的内容,红色不透明球体必须首先绘制,而绿色和蓝色则能够随意修改顺序。这里为了演示不注意设置深度缓冲的危害,咱们故意先绘制最近的蓝色球体,再绘制绿色球体。
为了让这些球体有一点立体感,咱们使用光照。在(1, 1, -1)处设置一个白色的光源。代码以下:
void setLight(void)
{
     static const GLfloat light_position[] = {1.0f, 1.0f, -1.0f, 1.0f};
     static const GLfloat light_ambient[]   = {0.2f, 0.2f, 0.2f, 1.0f};
     static const GLfloat light_diffuse[]   = {1.0f, 1.0f, 1.0f, 1.0f};
     static const GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

     glLightfv(GL_LIGHT0, GL_POSITION, light_position);
     glLightfv(GL_LIGHT0, GL_AMBIENT,   light_ambient);
     glLightfv(GL_LIGHT0, GL_DIFFUSE,   light_diffuse);
     glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);

     glEnable(GL_LIGHT0);
     glEnable(GL_LIGHTING);
     glEnable(GL_DEPTH_TEST);
}
每个球体颜色不一样。因此它们的材质也都不一样。这里用一个函数来设置材质。
void setMatirial(const GLfloat mat_diffuse[4], GLfloat mat_shininess)
{
     static const GLfloat mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};
     static const GLfloat mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};

     glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);
     glMaterialfv(GL_FRONT, GL_SPECULAR,   mat_specular);
     glMaterialfv(GL_FRONT, GL_EMISSION,   mat_emission);
     glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}
有了这两个函数,咱们就能够根据前面的知识写出整个程序代码了。这里只给出了绘制的部分,其它部分你们能够自行完成。
void myDisplay(void)
{
     // 定义一些材质颜色
     const static GLfloat red_color[] = {1.0f, 0.0f, 0.0f, 1.0f};
     const static GLfloat green_color[] = {0.0f, 1.0f, 0.0f, 0.3333f};
     const static GLfloat blue_color[] = {0.0f, 0.0f, 1.0f, 0.5f};

     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 启动混合并设置混合因子
     glEnable(GL_BLEND);
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

     // 设置光源
     setLight();

     // 以(0, 0, 0.5)为中心,绘制一个半径为.3的不透明红色球体(离观察者最远)
     setMatirial(red_color, 30.0);
     glPushMatrix();
     glTranslatef(0.0f, 0.0f, 0.5f);
     glutSolidSphere(0.3, 30, 30);
     glPopMatrix();

     // 下面将绘制半透明物体了,所以将深度缓冲设置为只读
     glDepthMask(GL_FALSE);

     // 以(0.2, 0, -0.5)为中心,绘制一个半径为.2的半透明蓝色球体(离观察者最近)
     setMatirial(blue_color, 30.0);
     glPushMatrix();
     glTranslatef(0.2f, 0.0f, -0.5f);
     glutSolidSphere(0.2, 30, 30);
     glPopMatrix();

     // 以(0.1, 0, 0)为中心,绘制一个半径为.15的半透明绿色球体(在前两个球体之间)
     setMatirial(green_color, 30.0);
     glPushMatrix();
     glTranslatef(0.1, 0, 0);
     glutSolidSphere(0.15, 30, 30);
     glPopMatrix();

     // 完成半透明物体的绘制,将深度缓冲区恢复为可读可写的形式
     glDepthMask(GL_TRUE);

     glutSwapBuffers();
}

你们也能够将上面两处glDepthMask删去,结果会看到最近的蓝色球虽然是半透明的,但它的背后直接就是红色球了,中间的绿色球没有被正确绘制。

http://blog.programfan.com/upfile/200704/20070406022744.jpghttp://blog.programfan.com/upfile/200704/20070406022749.jpg
小结:
本课介绍了OpenGL混合功能的相关知识。
混合就是在绘制时,不是直接把新的颜色覆盖在原来旧的颜色上,而是将新的颜色与旧的颜色通过必定的运算,从而产生新的颜色。新的颜色称为源颜色,原来旧的颜色称为目标颜色。传统意义上的混合,是将源颜色乘以源因子,目标颜色乘以目标因子,而后相加。
源因子和目标因子是能够设置的。源因子和目标因子设置的不一样直接致使混合结果的不一样。将源颜色的alpha值做为源因子,用1.0减去源颜色alpha值做为目标因子,是一种经常使用的方式。这时候,源颜色的alpha值至关于“不透明度”的做用。利用这一特色能够绘制出一些半透明的物体。
在进行混合时,绘制的顺序十分重要。由于在绘制时,正要绘制上去的是源颜色,原来存在的是目标颜色,所以先绘制的物体就成为目标颜色,后来绘制的则成为源颜色。绘制的顺序要考虑清楚,将目标颜色和设置的目标因子相对应,源颜色和设置的源因子相对应。
在进行三维混合时,不只要考虑源因子和目标因子,还应该考虑深度缓冲区。必须先绘制全部不透明的物体,再绘制半透明的物体。在绘制半透明物体时前,还须要将深度缓冲区设置为只读形式,不然可能出现画面错误。





OpenGL入门学习[十]


今天咱们先简单介绍Windows中经常使用的BMP文件格式,而后讲OpenGL的像素操做。虽然看起来内容可能有点多,但实际只有少许几个知识点,若是读者对诸如“显示BMP图象”等内容比较感兴趣的话,可能不知不觉就看完了。
像素操做能够很复杂,这里仅涉及了简单的部分,让你们对OpenGL像素操做有初步的印象。
学过多媒体技术的朋友可能知道,计算机保存图象的方法一般有两种:一是“矢量图”,一是“像素图”。矢量图保存了图象中每一几何物体的位置、形状、大小等信息,在显示图象时,根据这些信息计算获得完整的图象。“像素图”是将完整的图象纵横分为若干的行、列,这些行列使得图象被分割为很细小的分块,每一分块称为像素,保存每一像素的颜色也就保存了整个图象。
这两种方法各有优缺点。“矢量图”在图象进行放大、缩小时很方便,不会失真,但若是图象很复杂,那么就须要用很是多的几何体,数据量和运算量都很庞大。“像素图”不管图象多么复杂,数据量和运算量都不会增长,但在进行放大、缩小等操做时,会产生失真的状况。
前面咱们曾介绍了如何使用OpenGL来绘制几何体,咱们经过重复的绘制许多几何体,能够绘制出一幅矢量图。那么,应该如何绘制像素图呢?这就是咱们今天要学习的内容了。
一、BMP文件格式简单介绍
BMP文件是一种像素文件,它保存了一幅图象中全部的像素。这种文件格式能够保存单色位图、16色或256色索引模式像素图、24位真彩色图象,每种模式种单一像素的大小分别为1/8字节,1/2字节,1字节和3字节。目前最多见的是256色BMP和24位色BMP。这种文件格式还定义了像素保存的几种方法,包括不压缩、RLE压缩等。常见的BMP文件大可能是不压缩的。
这里为了简单起见,咱们仅讨论24位色、不使用压缩的BMP。(若是你使用Windows自带的画图程序,很容易绘制出一个符合以上要求的BMP)
Windows所使用的BMP文件,在开始处有一个文件头,大小为54字节。保存了包括文件格式标识、颜色数、图象大小、压缩方式等信息,由于咱们仅讨论24位色不压缩的BMP,因此文件头中的信息基本不须要注意,只有“大小”这一项对咱们比较有用。图象的宽度和高度都是一个32位整数,在文件中的地址分别为0x0012和0x0016,因而咱们能够使用如下代码来读取图象的大小信息:

GLint width, height; // 使用OpenGL的GLint类型,它是32位的。
                      // 而C语言自己的int则不必定是32位的。
FILE* pFile;
// 在这里进行“打开文件”的操做
fseek(pFile, 0x0012, SEEK_SET);          // 移动到0x0012位置
fread(&width, sizeof(width), 1, pFile); // 读取宽度
fseek(pFile, 0x0016, SEEK_SET);          // 移动到0x0016位置
                                         // 因为上一句执行后本就应该在0x0016位置
                                         // 因此这一句可省略
fread(&height, sizeof(height), 1, pFile); // 读取高度

54个字节之后,若是是16色或256色BMP,则还有一个颜色表,但24位色BMP没有这个,咱们这里不考虑。接下来就是实际的像素数据了。24位色的BMP文件中,每三个字节表示一个像素的颜色。
注意,OpenGL一般使用RGB来表示颜色,但BMP文件则采用BGR,就是说,顺序被反过来了。
另外须要注意的地方是:像素的数据量并不必定彻底等于图象的高度乘以宽度乘以每一像素的字节数,而是可能略大于这个值。缘由是BMP文件采用了一种“对齐”的机制,每一行像素数据的长度若不是4的倍数,则填充一些数据使它是4的倍数。这样一来,一个17*15的24位BMP大小就应该是834字节(每行17个像素,有51字节,补充为52字节,乘以15获得像素数据总长度780,再加上文件开始的54字节,获得834字节)。分配内存时,必定要当心,不能直接使用“图象的高度乘以宽度乘以每一像素的字节数”来计算分配空间的长度,不然有可能致使分配的内存空间长度不足,形成越界访问,带来各类严重后果。
一个很简单的计算数据长度的方法以下:

int LineLength, TotalLength;
LineLength = ImageWidth * BytesPerPixel; // 每行数据长度大体为图象宽度乘以
                                          // 每像素的字节数
while( LineLength % 4 != 0 )              // 修正LineLength使其为4的倍数
     ++LineLenth;
TotalLength = LineLength * ImageHeight;   // 数据总长 = 每行长度 * 图象高度

这并非效率最高的方法,但因为这个修正自己运算量并不大,使用频率也不高,咱们就不须要再考虑更快的方法了。
二、简单的OpenGL像素操做
OpenGL提供了简洁的函数来操做像素:
glReadPixels:读取一些像素。当前能够简单理解为“把已经绘制好的像素(它可能已经被保存到显卡的显存中)读取到内存”。
glDrawPixels:绘制一些像素。当前能够简单理解为“把内存中一些数据做为像素数据,进行绘制”。
glCopyPixels:复制一些像素。当前能够简单理解为“把已经绘制好的像素从一个位置复制到另外一个位置”。虽然从功能上看,好象等价于先读取像素再绘制像素,但实际上它不须要把已经绘制的像素(它可能已经被保存到显卡的显存中)转换为内存数据,而后再由内存数据进行从新的绘制,因此要比先读取后绘制快不少。
这三个函数能够完成简单的像素读取、绘制和复制任务,但实际上也能够完成更复杂的任务。当前,咱们仅讨论一些简单的应用。因为这几个函数的参数数目比较多,下面咱们分别介绍。
三、glReadPixels的用法和举例
3.1 函数的参数说明
该函数总共有七个参数。前四个参数能够获得一个矩形,该矩形所包括的像素都会被读取出来。(第1、二个参数表示了矩形的左下角横、纵坐标,坐标以窗口最左下角为零,最右上角为最大值;第3、四个参数表示了矩形的宽度和高度)
第五个参数表示读取的内容,例如:GL_RGB就会依次读取像素的红、绿、蓝三种数据,GL_RGBA则会依次读取像素的红、绿、蓝、alpha四种数据,GL_RED则只读取像素的红色数据(相似的还有GL_GREEN,GL_BLUE,以及GL_ALPHA)。若是采用的不是RGBA颜色模式,而是采用颜色索引模式,则也能够使用GL_COLOR_INDEX来读取像素的颜色索引。目前仅须要知道这些,但实际上还能够读取其它内容,例如深度缓冲区的深度数据等。
第六个参数表示读取的内容保存到内存时所使用的格式,例如:GL_UNSIGNED_BYTE会把各类数据保存为GLubyte,GL_FLOAT会把各类数据保存为GLfloat等。
第七个参数表示一个指针,像素数据被读取后,将被保存到这个指针所表示的地址。注意,须要保证该地址有足够的能够使用的空间,以容纳读取的像素数据。例如一幅大小为256*256的图象,若是读取其RGB数据,且每一数据被保存为GLubyte,总大小就是:256*256*3 = 196608字节,即192千字节。若是是读取RGBA数据,则总大小就是256*256*4 = 262144字节,即256千字节。

注意:glReadPixels其实是从缓冲区中读取数据,若是使用了双缓冲区,则默认是从正在显示的缓冲(即前缓冲)中读取,而绘制工做是默认绘制到后缓冲区的。所以,若是须要读取已经绘制好的像素,每每须要先交换先后缓冲。

再看前面提到的BMP文件中两个须要注意的地方:
3.2 解决OpenGL经常使用的RGB像素数据与BMP文件的BGR像素数据顺序不一致问题
能够使用一些代码交换每一个像素的第一字节和第三字节,使得RGB的数据变成BGR的数据。固然也能够使用另外的方式解决问题:新版本的OpenGL除了能够使用GL_RGB读取像素的红、绿、蓝数据外,也能够使用GL_BGR按照相反的顺序依次读取像素的蓝、绿、红数据,这样就与BMP文件格式相吻合了。即便你的gl/gl.h头文件中没有定义这个GL_BGR,也没有关系,能够尝试使用GL_BGR_EXT。虽然有的OpenGL实现(尤为是旧版本的实现)并不能使用GL_BGR_EXT,但我所知道的Windows环境下各类OpenGL实现都对GL_BGR提供了支持,毕竟Windows中各类表示颜色的数据几乎都是使用BGR的顺序,而非RGB的顺序。这可能与IBM-PC的硬件设计有关。

3.3 消除BMP文件中“对齐”带来的影响
实际上OpenGL也支持使用了这种“对齐”方式的像素数据。只要经过glPixelStore修改“像素保存时对齐的方式”就能够了。像这样:
int alignment = 4;
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
第一个参数表示“设置像素的对齐值”,第二个参数表示实际设置为多少。这里像素能够单字节对齐(实际上就是不使用对齐)、双字节对齐(若是长度为奇数,则再补一个字节)、四字节对齐(若是长度不是四的倍数,则补为四的倍数)、八字节对齐。分别对应alignment的值为1, 2, 4, 8。实际上,默认的值是4,正好与BMP文件的对齐方式相吻合。
glPixelStorei也能够用于设置其它各类参数。但咱们这里并不须要深刻讨论了。


如今,咱们已经能够把屏幕上的像素读取到内存了,若是须要的话,咱们还能够将内存中的数据保存到文件。正确的对照BMP文件格式,咱们的程序就能够把屏幕中的图象保存为BMP文件,达到屏幕截图的效果。
咱们并无详细介绍BMP文件开头的54个字节的全部内容,不过这无伤大雅。从一个正确的BMP文件中读取前54个字节,修改其中的宽度和高度信息,就能够获得新的文件头了。假设咱们先创建一个1*1大小的24位色BMP,文件名为dummy.bmp,又假设新的BMP文件名称为grab.bmp。则能够编写以下代码:

FILE* pOriginFile = fopen("dummy.bmp", "rb);
FILE* pGrabFile = fopen("grab.bmp", "wb");
char   BMP_Header[54];
GLint width, height;

/* 先在这里设置好图象的宽度和高度,即width和height的值,并计算像素的总长度 */

// 读取dummy.bmp中的头54个字节到数组
fread(BMP_Header, sizeof(BMP_Header), 1, pOriginFile);
// 把数组内容写入到新的BMP文件
fwrite(BMP_Header, sizeof(BMP_Header), 1, pGrabFile);

// 修改其中的大小信息
fseek(pGrabFile, 0x0012, SEEK_SET);
fwrite(&width, sizeof(width), 1, pGrabFile);
fwrite(&height, sizeof(height), 1, pGrabFile);

// 移动到文件末尾,开始写入像素数据
fseek(pGrabFile, 0, SEEK_END);

/* 在这里写入像素数据到文件 */

fclose(pOriginFile);
fclose(pGrabFile);
咱们给出完整的代码,演示如何把整个窗口的图象抓取出来并保存为BMP文件。

#define WindowWidth   400
#define WindowHeight 400

#include <stdio.h>
#include <stdlib.h>

/* 函数grab
* 抓取窗口中的像素
* 假设窗口宽度为WindowWidth,高度为WindowHeight
*/
#define BMP_Header_Length 54
void grab(void)
{
     FILE*     pDummyFile;
     FILE*     pWritingFile;
     GLubyte* pPixelData;
     GLubyte   BMP_Header[BMP_Header_Length];
     GLint     i, j;
     GLint     PixelDataLength;

     // 计算像素数据的实际长度
     i = WindowWidth * 3;    // 获得每一行的像素数据长度
     while( i%4 != 0 )       // 补充数据,直到i是的倍数
         ++i;                // 原本还有更快的算法,
                            // 但这里仅追求直观,对速度没有过高要求
     PixelDataLength = i * WindowHeight;

     // 分配内存和打开文件
     pPixelData = (GLubyte*)malloc(PixelDataLength);
     if( pPixelData == 0 )
         exit(0);

     pDummyFile = fopen("dummy.bmp", "rb");
     if( pDummyFile == 0 )
         exit(0);

     pWritingFile = fopen("grab.bmp", "wb");
     if( pWritingFile == 0 )
         exit(0);

     // 读取像素
     glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
     glReadPixels(0, 0, WindowWidth, WindowHeight,
         GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);

     // 把dummy.bmp的文件头复制为新文件的文件头
     fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
     fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
     fseek(pWritingFile, 0x0012, SEEK_SET);
     i = WindowWidth;
     j = WindowHeight;
     fwrite(&i, sizeof(i), 1, pWritingFile);
     fwrite(&j, sizeof(j), 1, pWritingFile);

     // 写入像素数据
     fseek(pWritingFile, 0, SEEK_END);
     fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

     // 释放内存和关闭文件
     fclose(pDummyFile);
     fclose(pWritingFile);
     free(pPixelData);
}



把这段代码复制到之前任何课程的样例程序中,在绘制函数的最后调用grab函数,便可把图象内容保存为BMP文件了。(在我写这个教程的时候,很多地方都用这样的代码进行截图工做,这段代码一旦写好,运行起来是很方便的。)
四、glDrawPixels的用法和举例
glDrawPixels函数与glReadPixels函数相比,参数内容大体相同。它的第1、2、3、四个参数分别对应于glReadPixels函数的第3、4、5、六个参数,依次表示图象宽度、图象高度、像素数据内容、像素数据在内存中的格式。两个函数的最后一个参数也是对应的,glReadPixels中表示像素读取后存放在内存中的位置,glDrawPixels则表示用于绘制的像素数据在内存中的位置。
注意到glDrawPixels函数比glReadPixels函数少了两个参数,这两个参数在glReadPixels中分别是表示图象的起始位置。在glDrawPixels中,没必要显式的指定绘制的位置,这是由于绘制的位置是由另外一个函数glRasterPos*来指定的。glRasterPos*函数的参数与glVertex*相似,经过指定一个二维/三维/四维坐标,OpenGL将自动计算出该坐标对应的屏幕位置,并把该位置做为绘制像素的起始位置。
很天然的,咱们能够从BMP文件中读取像素数据,并使用glDrawPixels绘制到屏幕上。咱们选择Windows XP默认的桌面背景Bliss.bmp做为绘制的内容(若是你使用的是Windows XP系统,极可能能够在硬盘中搜索到这个文件。固然你也能够使用其它BMP文件来代替,只要它是24位的BMP文件。注意须要修改代码开始部分的FileName的定义),先把该文件复制一份放到正确的位置,咱们在程序开始时,就读取该文件,从而得到图象的大小后,根据该大小来建立合适的OpenGL窗口,并绘制像素。
绘制像素原本是很简单的过程,可是这个程序在骨架上与前面的各类示例程序稍有不一样,因此我仍是打算给出一份完整的代码。

#include <gl/glut.h>

#define FileName "Bliss.bmp"

static GLint     ImageWidth;
static GLint     ImageHeight;
static GLint     PixelLength;
static GLubyte* PixelData;

#include <stdio.h>
#include <stdlib.h>

void display(void)
{
     // 清除屏幕并没必要要
     // 每次绘制时,画面都覆盖整个屏幕
     // 所以不管是否清除屏幕,结果都同样
     // glClear(GL_COLOR_BUFFER_BIT);

     // 绘制像素
     glDrawPixels(ImageWidth, ImageHeight,
         GL_BGR_EXT, GL_UNSIGNED_BYTE, PixelData);

     // 完成绘制
     glutSwapBuffers();
}

int main(int argc, char* argv[])
{
     // 打开文件
     FILE* pFile = fopen("Bliss.bmp", "rb");
     if( pFile == 0 )
         exit(0);

     // 读取图象的大小信息
     fseek(pFile, 0x0012, SEEK_SET);
     fread(&ImageWidth, sizeof(ImageWidth), 1, pFile);
     fread(&ImageHeight, sizeof(ImageHeight), 1, pFile);

     // 计算像素数据长度
     PixelLength = ImageWidth * 3;
     while( PixelLength % 4 != 0 )
         ++PixelLength;
     PixelLength *= ImageHeight;

     // 读取像素数据
     PixelData = (GLubyte*)malloc(PixelLength);
     if( PixelData == 0 )
         exit(0);

     fseek(pFile, 54, SEEK_SET);
     fread(PixelData, PixelLength, 1, pFile);

     // 关闭文件
     fclose(pFile);

     // 初始化GLUT并运行
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(ImageWidth, ImageHeight);
     glutCreateWindow(FileName);
     glutDisplayFunc(&display);
     glutMainLoop();

     // 释放内存
     // 实际上,glutMainLoop函数永远不会返回,这里也永远不会到达
     // 这里写释放内存只是出于一种我的习惯
     // 不用担忧内存没法释放。在程序结束时操做系统会自动回收全部内存
     free(PixelData);

     return 0;
}



这里仅仅是一个简单的显示24位BMP图象的程序,若是读者对BMP文件格式比较熟悉,也能够写出适用于各类BMP图象的显示程序,在像素处理时,它们所使用的方法是相似的。
OpenGL在绘制像素以前,能够对像素进行若干处理。最经常使用的可能就是对整个像素图象进行放大/缩小。使用glPixelZoom来设置放大/缩小的系数,该函数有两个参数,分别是水平方向系数和垂直方向系数。例如设置glPixelZoom(0.5f, 0.8f);则表示水平方向变为原来的50%大小,而垂直方向变为原来的80%大小。咱们甚至能够使用负的系数,使得整个图象进行水平方向或垂直方向的翻转(默认像素从左绘制到右,但翻转后将从右绘制到左。默认像素从下绘制到上,但翻转后将从上绘制到下。所以,glRasterPos*函数设置的“开始位置”不必定就是矩形的左下角)。
五、glCopyPixels的用法和举例
从效果上看,glCopyPixels进行像素复制的操做,等价于把像素读取到内存,再从内存绘制到另外一个区域,所以能够经过glReadPixels和glDrawPixels组合来实现复制像素的功能。然而咱们知道,像素数据一般数据量很大,例如一幅1024*768的图象,若是使用24位BGR方式表示,则须要至少1024*768*3字节,即2.25兆字节。这么多的数据要进行一次读操做和一次写操做,而且由于在glReadPixels和glDrawPixels中设置的数据格式不一样,极可能涉及到数据格式的转换。这对CPU无疑是一个不小的负担。使用glCopyPixels直接从像素数据复制出新的像素数据,避免了多余的数据的格式转换,而且也可能减小一些数据复制操做(由于数据可能直接由显卡负责复制,不须要通过主内存),所以效率比较高。
glCopyPixels函数也经过glRasterPos*系列函数来设置绘制的位置,由于不须要涉及到主内存,因此不须要指定数据在内存中的格式,也不须要使用任何指针。
glCopyPixels函数有五个参数,第1、二个参数表示复制像素来源的矩形的左下角坐标,第3、四个参数表示复制像素来源的举行的宽度和高度,第五个参数一般使用GL_COLOR,表示复制像素的颜色,但也能够是GL_DEPTH或GL_STENCIL,分别表示复制深度缓冲数据或模板缓冲数据。
值得一提的是,glDrawPixels和glReadPixels中设置的各类操做,例如glPixelZoom等,在glCopyPixels函数中一样有效。
下面看一个简单的例子,绘制一个三角形后,复制像素,并同时进行水平和垂直方向的翻转,而后缩小为原来的一半,并绘制。绘制完毕后,调用前面的grab函数,将屏幕中全部内容保存为grab.bmp。其中WindowWidth和WindowHeight是表示窗口宽度和高度的常量。

void display(void)
{
     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT);

     // 绘制
     glBegin(GL_TRIANGLES);
         glColor3f(1.0f, 0.0f, 0.0f);     glVertex2f(0.0f, 0.0f);
         glColor3f(0.0f, 1.0f, 0.0f);     glVertex2f(1.0f, 0.0f);
         glColor3f(0.0f, 0.0f, 1.0f);     glVertex2f(0.5f, 1.0f);
     glEnd();
     glPixelZoom(-0.5f, -0.5f);
     glRasterPos2i(1, 1);
     glCopyPixels(WindowWidth/2, WindowHeight/2,
         WindowWidth/2, WindowHeight/2, GL_COLOR);

     // 完成绘制,并抓取图象保存为BMP文件
     glutSwapBuffers();
     grab();
}



http://blog.programfan.com/upfile/200704/20070419202924.jpg
小结:
本课结合Windows系统常见的BMP图象格式,简单介绍了OpenGL的像素处理功能。包括使用glReadPixels读取像素、glDrawPixels绘制像素、glCopyPixels复制像素。
本课仅介绍了像素处理的一些简单应用,但相信你们已经能够体会到,围绕这三个像素处理函数,还存在一些“外围”函数,好比glPixelStore*,glRasterPos*,以及glPixelZoom等。咱们仅使用了这些函数的一少部分功能。
本课内容并很少,例子足够丰富,三个像素处理函数都有例子,你们能够结合例子来体会。



OpenGL入门学习[十一]


咱们在前一课中,学习了简单的像素操做,这意味着咱们能够使用各类各样的BMP文件来丰富程序的显示效果,因而咱们的OpenGL图形程序也再也不像之前老是只显示几个多边形那样单调了。——可是这还不够。虽然咱们能够将像素数据按照矩形进行缩小和放大,可是还不足以知足咱们的要求。例如要将一幅世界地图绘制到一个球体表面,只使用glPixelZoom这样的函数来进行缩放显然是不够的。OpenGL纹理映射功能支持将一些像素数据通过变换(即便是比较不规则的变换)将其附着到各类形状的多边形表面。纹理映射功能十分强大,利用它能够实现目前计算机动画中的大多数效果,可是它也很复杂,咱们不可能一次性的彻底讲解。这里的课程只是关于二维纹理的简单使用。但即便是这样,也会使咱们的程序在显示效果上迈出一大步。
下面几张图片说明了纹理的效果。前两张是咱们须要的纹理,后一张是咱们使用纹理后,利用OpenGL所产生出的效果。

http://blog.programfan.com/upfile/200707/20070730074740.jpg
http://blog.programfan.com/upfile/200707/20070730074746.jpg
http://blog.programfan.com/upfile/200707/20070730074751.jpg

纹理的使用是很是复杂的。所以即便是入门教程,在编写时我也屡次进行删改,不少东西都被精简掉了,但本课的内容仍然较多,你们要有一点心理准备~
一、启用纹理和载入纹理
就像咱们曾经学习过的OpenGL光照、混合等功能同样。在使用纹理前,必须启用它。OpenGL支持一维纹理、二维纹理和三维纹理,这里咱们仅介绍二维纹理。能够使用如下语句来启用和禁用二维纹理:

     glEnable(GL_TEXTURE_2D);   // 启用二维纹理
     glDisable(GL_TEXTURE_2D); // 禁用二维纹理



使用纹理前,还必须载入纹理。利用glTexImage2D函数能够载入一个二维的纹理,该函数有多达九个参数(虽然某些参数咱们能够暂时不去了解),如今分别说明以下:
第一个参数为指定的目标,在咱们的入门教材中,这个参数将始终使用GL_TEXTURE_2D。
第二个参数为“多重细节层次”,如今咱们并不考虑多重纹理细节,所以这个参数设置为零。
第三个参数有两种用法。在OpenGL 1.0,即最初的版本中,使用整数来表示颜色份量数目,例如:像素数据用RGB颜色表示,总共有红、绿、蓝三个值,所以参数设置为3,而若是像素数据是用RGBA颜色表示,总共有红、绿、蓝、alpha四个值,所以参数设置为4。而在后来的版本中,能够直接使用GL_RGB或GL_RGBA来表示以上状况,显得更直观(并带来其它一些好处,这里暂时不提)。注意:虽然咱们使用Windows的BMP文件做为纹理时,通常是蓝色的像素在最前,其真实的格式为GL_BGR而不是GL_RGB,在数据的顺序上有所不一样,但由于一样是红、绿、蓝三种颜色,所以这里仍然使用GL_RGB。(若是使用GL_BGR,OpenGL将没法识别这个参数,形成错误)
第4、五个参数是二维纹理像素的宽度和高度。这里有一个很须要注意的地方:OpenGL在之前的不少版本中,限制纹理的大小必须是2的整数次方,即纹理的宽度和高度只能是16, 32, 64, 128, 256等值,直到最近的新版本才取消了这个限制。并且,一些OpenGL实现(例如,某些PC机上板载显卡的驱动程序附带的OpenGL)并无支持到如此高的OpenGL版本。所以在使用纹理时要特别注意其大小。尽可能使用大小为2的整数次方的纹理,当这个要求没法知足时,使用gluScaleImage函数把图象缩放至所指定的大小(在后面的例子中有用到)。另外,不管旧版本仍是新版本,都限制了纹理大小的最大值,例如,某OpenGL实现可能要求纹理最大不能超过1024*1024。能够使用以下的代码来得到OpenGL所支持的最大纹理:

GLint  max;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, & max);


这样max的值就是当前OpenGL实现中所支持的最大纹理。
在很长一段时间内,不少图形程序都喜欢使用256*256大小的纹理,不只由于256是2的整数次方,也由于某些硬件能够使用8位的整数来表示纹理坐标,2的8次方正好是256,这一巧妙的组合为处理纹理坐标时的硬件优化创造了一些不错的条件。

第六个参数是纹理边框的大小,咱们没有使用纹理边框,所以这里设置为零。
最后三个参数与glDrawPixels函数的最后三个参数的使用方法相同,其含义能够参考glReadPixels的最后三个参数。你们能够复习一下第10课的相关内容,这里再也不重复。
举个例子,若是有一幅大小为width*height,格式为Windows系统中使用最广泛的24位BGR,保存在pixels中的像素图象。则把这样一幅图象载入为纹理可以使用如下代码:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);



注意,载入纹理的过程可能比较慢,缘由是纹理数据一般比较大,例如一幅512*512的BGR格式的图象,大小为0.75M。把这些像素数据从主内存传送到专门的图形硬件,这个过程当中还可能须要把程序中所指定的像素格式转化为图形硬件所能识别的格式(或最能发挥图形硬件性能的格式),这些操做都须要较多时间。
二、纹理坐标
咱们先来回忆一下以前学过的一点内容:
当咱们绘制一个三角形时,只须要指定三个顶点的颜色。三角形中其它各点的颜色不须要咱们指定,这些点的颜色是OpenGL本身经过计算获得的。
在咱们学习OpneGL光照时,法线向量、材质的指定,都是只须要在顶点处指定一下就能够了,其它地方的法线向量和材质都是OpenGL本身经过计算去得到。

纹理的使用方法也与此相似。只要指定每个顶点在纹理图象中所对应的像素位置,OpenGL就会自动计算顶点之外的其它点在纹理图象中所对应的像素位置。
这听起来比较使人迷惑。咱们能够这样类比一下:
在绘制一条线段时,咱们设置其中一个端点为红色,另外一个端点为绿色,则OpenGL会自动计算线段中其它各像素的颜色,若是是使用glShadeMode(GL_SMOOTH);,则最终会造成一种渐变的效果(例如线段中点,就是红色和绿色的中间色)。
相似的,在绘制一条线段时,咱们设置其中一个端点使用“纹理图象中最左下角的颜色”做为它的颜色,另外一个端点使用“纹理图象中最右上角的颜色”做为它的颜色,则OpenGL会自动在纹理图象中选择合适位置的颜色,填充到线段的各个像素(例如线段中点,可能就是选择纹理图象中央的那个像素的颜色)。

咱们在类比时,使用了“纹理图象中最左下角的颜色”这种说法。但这种说法在不少时候不够精确,咱们须要一种精确的方式来表示咱们究竟使用纹理中的哪一个像素。纹理坐标也就是由于这样的要求而产生的。以二维纹理为例,规定纹理最左下角的坐标为(0, 0),最右上角的坐标为(1, 1),因而纹理中的每个像素的位置均可以用两个浮点数来表示(三维纹理会用三个浮点数表示,一维纹理则只用一个便可)。
使用glTexCoord*系列函数来指定纹理坐标。这些函数的用法与使用glVertex*系列函数来指定顶点坐标十分类似。例如:glTexCoord2f(0.0f, 0.0f);指定使用(0, 0)纹理坐标。
一般,每一个顶点使用不一样的纹理,因而下面这样形式的代码是比较常见的。

glBegin( /* ... */ );
     glTexCoord2f( /* ... */ );   glVertex3f( /* ... */ );
     glTexCoord2f( /* ... */ );   glVertex3f( /* ... */ );
     /* ... */
glEnd();



当咱们用一个坐标表示顶点在三维空间的位置时,能够使用glRotate*等函数来对坐标进行转换。纹理坐标也能够进行这种转换。只要使用glMatrixMode(GL_TEXTURE);,就能够切换到纹理矩阵(另外还有透视矩阵GL_PROJECTION和模型视图矩阵GL_MODELVIEW,详细状况在第五课有讲述),而后glRotate*,glScale*,glTranslate*等操做矩阵的函数就能够用来处理“对纹理坐标进行转换”的工做了。在简单应用中,可能不会对矩阵进行任何变换,这样考虑问题会比较简单。
三、纹理参数
到这里,入门所须要掌握的全部难点都被咱们掌握了。可是,咱们的知识仍然是不够的,若是仅利用现有的知识去使用纹理的话,你可能会发现纹理彻底不起做用。这是由于在使用纹理前还有某些参数是必须设置的。
使用glTexParameter*系列函数来设置纹理参数。一般须要设置下面四个参数:
GL_TEXTURE_MAG_FILTER:指当纹理图象被使用到一个大于它的形状上时(即:有可能纹理图象中的一个像素会被应用到实际绘制时的多个像素。例如将一幅256*256的纹理图象应用到一个512*512的正方形),应该如何处理。可选择的设置有GL_NEAREST和GL_LINEAR,前者表示“使用纹理中坐标最接近的一个像素的颜色做为须要绘制的像素颜色”,后者表示“使用纹理中坐标最接近的若干个颜色,经过加权平均算法获得须要绘制的像素颜色”。前者只通过简单比较,须要运算较少,可能速度较快,后者须要通过加权平均计算,其中涉及除法运算,可能速度较慢(但若是有专门的处理硬件,也可能二者速度相同)。从视觉效果上看,前者效果较差,在一些状况下锯齿现象明显,后者效果会较好(但若是纹理图象自己比较大,则二者在视觉效果上就会比较接近)。
GL_TEXTURE_MIN_FILTER:指当纹理图象被使用到一个小于(或等于)它的形状上时(即有可能纹理图象中的多个像素被应用到实际绘制时的一个像素。例如将一幅256*256的纹理图象应用到一个128*128的正方形),应该如何处理。可选择的设置有GL_NEAREST,GL_LINEAR,GL_NEAREST_MIPMAP_NEAREST,GL_NEAREST_MIPMAP_LINEAR,GL_LINEAR_MIPMAP_NEAREST和GL_LINEAR_MIPMAP_LINEAR。其中后四个涉及到mipmap,如今暂时不须要了解。前两个选项则和GL_TEXTURE_MAG_FILTER中的相似。此参数彷佛是必须设置的(在个人计算机上,不设置此参数将获得错误的显示结果,但我目前并无找到根据)。
GL_TEXTURE_WRAP_S:指当纹理坐标的第一维坐标值大于1.0或小于0.0时,应该如何处理。基本的选项有GL_CLAMP和GL_REPEAT,前者表示“截断”,即超过1.0的按1.0处理,不足0.0的按0.0处理。后者表示“重复”,即对坐标值加上一个合适的整数(能够是正数或负数),获得一个在[0.0, 1.0]范围内的值,而后用这个值做为新的纹理坐标。例如:某二维纹理,在绘制某形状时,一像素须要获得纹理中坐标为(3.5, 0.5)的像素的颜色,其中第一维的坐标值3.5超过了1.0,则在GL_CLAMP方式中将被转化为(1.0, 0.5),在GL_REPEAT方式中将被转化为(0.5, 0.5)。在后来的OpenGL版本中,又增长了新的处理方式,这里不作介绍。若是不指定这个参数,则默认为GL_REPEAT。
GL_TEXTURE_WRAP_T:指当纹理坐标的第二维坐标值大于1.0或小于0.0时,应该如何处理。选项与GL_TEXTURE_WRAP_S相似,再也不重复。若是不指定这个参数,则默认为GL_REPEAT。

设置参数的代码以下所示:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

四、纹理对象
前面已经提到过,载入一幅纹理所须要的时间是比较多的。所以应该尽可能减小载入纹理的次数。若是只有一幅纹理,则应该在第一次绘制前就载入它,之后就不须要再次载入了。这点与glDrawPixels函数很不相同。每次使用glDrawPixels函数,都须要把像素数据从新载入一次,所以用glDrawPixels函数来反复绘制图象的效率是较低的(若是只绘制一次,则不会有此问题),使用纹理来反复绘制图象是可取的作法。
可是,在每次绘制时要使用两幅或更多幅的纹理时,这个办法就行不通了。你可能会编写下面的代码:

glTexImage2D( /* ... */ ); // 载入第一幅纹理
// 使用第一幅纹理
glTexImage2D( /* ... */ ); // 载入第二幅纹理
// 使用第二幅纹理
// 当纹理的数量增长时,这段代码会变得更加复杂。



在绘制动画时,因为每秒钟须要将画面绘制数十次,所以若是使用上面的代码,就会反复载入纹理,这对计算机是很是大的负担,以目前的我的计算机配置来讲,根本就没法让动画可以流畅的运行。所以,须要有一种机制,可以在不一样的纹理之间进行快速的切换。

纹理对象正是这样一种机制。咱们能够把每一幅纹理(包括纹理的像素数据、纹理大小等信息,也包括了前面所讲的纹理参数)放到一个纹理对象中,经过建立多个纹理对象来达到同时保存多幅纹理的目的。这样一来,在第一次使用纹理前,把全部的纹理都载入,而后在绘制时只须要指明究竟使用哪个纹理对象就能够了。

使用纹理对象和使用显示列表有类似之处:使用一个正整数来做为纹理对象的编号。在使用前,能够调用glGenTextures来分配纹理对象。该函数有两种比较常见的用法:

GLuint texture_ID;
glGenTextures(1, &texture_ID); // 分配一个纹理对象的编号


或者:

GLuint texture_ID_list[5];
glGenTextures(5, texture_ID_list); // 分配5个纹理对象的编号



零是一个特殊的纹理对象编号,表示“默认的纹理对象”,在分配正确的状况下,glGenTextures不会分配这个编号。与glGenTextures对应的是glDeleteTextures,用于销毁一个纹理对象。

在分配了纹理对象编号后,使用glBindTexture函数来指定“当前所使用的纹理对象”。而后就能够使用glTexImage*系列函数来指定纹理像素、使用glTexParameter*系列函数来指定纹理参数、使用glTexCoord*系列函数来指定纹理坐标了。若是不使用glBindTexture函数,那么glTexImage*、glTexParameter*、glTexCoord*系列函数默认在一个编号为0的纹理对象上进行操做。glBindTexture函数有两个参数,第一个参数是须要使用纹理的目标,由于咱们如今只学习二维纹理,因此指定为GL_TEXTURE_2D,第二个参数是所使用的纹理的编号。
使用多个纹理对象,就能够使OpenGL同时保存多个纹理。在使用时只须要调用glBindTexture函数,在不一样纹理之间进行切换,而不须要反复载入纹理,所以动画的绘制速度会有很是明显的提高。典型的代码以下所示:

// 在程序开始时:分配好纹理编号,并载入纹理
glGenTextures( /* ... */ );
glBindTexture(GL_TEXTURE_2D, texture_ID_1);
// 载入第一幅纹理
glBindTexture(GL_TEXTURE_2D, texture_ID_2);
// 载入第二幅纹理



// 在绘制时,切换并使用纹理,不须要再进行载入
glBindTexture(GL_TEXTURE_2D, texture_ID_1); // 指定第一幅纹理
// 使用第一幅纹理
glBindTexture(GL_TEXTURE_2D, texture_ID_2); // 指定第二幅纹理
// 使用第二幅纹理



提示:纹理对象是从OpenGL 1.1版开始才有的,最旧版本的OpenGL 1.0并无处理纹理对象的功能。不过,我想各位的机器不会是比OpenGL 1.1更低的版本(Windows 95就自带了OpenGL 1.1版本,遗憾的是,Microsoft对OpenGL的支持并不积极,Windows XP也还采用1.1版本。听说Vista使用的是OpenGL 1.4版。固然了,若是安装显卡驱动的话,如今的主流显卡通常都附带了适用于该显卡的OpenGL 1.4版或更高版本),因此这个问题也就不算是问题了。
五、示例程序
纹理入门所须要掌握的知识点就介绍到这里了。可是若是不实际动手操做的话,也是不可能真正掌握的。下面咱们来看看本课开头的那个纹理效果是如何实现的吧。
由于代码比较长,我把它拆分红了三段,你们若是要编译的话,应该把三段代码按顺序连在一块儿编译。若是要运行的话,除了要保证有一个名称为dummy.bmp,图象大小为1*1的24位BMP文件,还要把本课开始的两幅纹理图片保存到正确位置(一幅名叫ground.bmp,另外一幅名叫wall.bmp。注意:我为了节省网络空间,把两幅图片都转成jpg格式了,读者把图片保存到本地后,须要把它们再转化为BMP格式。能够使用Windows XP带的画图程序中的“另存为”功能完成这一转换)。
第一段代码以下。其中的主体——grab函数,是咱们在第十课介绍过的,这里仅仅是抄过来用一下,目的是为了将最终效果图保存到一个名字叫grab.bmp的文件中。(固然了,为了保证程序的正确运行,那个大小为1*1的dummy.bmp文件仍然是必要的,参见第十课)

#define WindowWidth   400
#define WindowHeight 400
#define WindowTitle   "OpenGL纹理测试"

#include <gl/glut.h>
#include <stdio.h>
#include <stdlib.h>

/* 函数grab
* 抓取窗口中的像素
* 假设窗口宽度为WindowWidth,高度为WindowHeight
*/
#define BMP_Header_Length 54
void grab( void)
{
     FILE*     pDummyFile;
     FILE*     pWritingFile;
     GLubyte* pPixelData;
     GLubyte   BMP_Header[BMP_Header_Length];
     GLint     i, j;
     GLint     PixelDataLength;

     // 计算像素数据的实际长度
     i = WindowWidth * 3;    // 获得每一行的像素数据长度
     while( i%4 != 0 )       // 补充数据,直到i是的倍数
         ++i;                // 原本还有更快的算法,
                            // 但这里仅追求直观,对速度没有过高要求
     PixelDataLength = i * WindowHeight;

     // 分配内存和打开文件
     pPixelData = (GLubyte*) malloc(PixelDataLength);
     if( pPixelData == 0 )
         exit(0);

     pDummyFile =  fopen( "dummy.bmp""rb");
     if( pDummyFile == 0 )
         exit(0);

     pWritingFile =  fopen( "grab.bmp""wb");
     if( pWritingFile == 0 )
         exit(0);

     // 读取像素
     glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
     glReadPixels(0, 0, WindowWidth, WindowHeight,
         GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);

     // 把dummy.bmp的文件头复制为新文件的文件头
     fread(BMP_Header,  sizeof(BMP_Header), 1, pDummyFile);
     fwrite(BMP_Header,  sizeof(BMP_Header), 1, pWritingFile);
     fseek(pWritingFile, 0x0012, SEEK_SET);
     i = WindowWidth;
     j = WindowHeight;
     fwrite(&i,  sizeof(i), 1, pWritingFile);
     fwrite(&j,  sizeof(j), 1, pWritingFile);

     // 写入像素数据
     fseek(pWritingFile, 0, SEEK_END);
     fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

     // 释放内存和关闭文件
     fclose(pDummyFile);
     fclose(pWritingFile);
     free(pPixelData);
}

第二段代码是咱们的重点。它包括两个函数。其中power_of_two比较简单,虽然实现手段有点奇特,但也并不是没法理解(即便真的没法理解,读者也能够给出本身的解决方案,用一些循环以及多使用一些位操做也不要紧。反正,这里不是重点啦)。另外一个load_texture函数倒是重头戏:打开BMP文件、读取其中的高度和宽度信息、计算像素数据所占的字节数、为像素数据分配空间、读取像素数据、对像素图象进行缩放(若是必要的话)、分配新的纹理编号、填写纹理参数、载入纹理,全部的功能都在同一个函数里面完成了。为了叙述方便,我把全部的解释都放在了注释里。

/* 函数power_of_two
* 检查一个整数是否为2的整数次方,若是是,返回1,不然返回0
* 实际上只要查看其二进制位中有多少个,若是正好有1个,返回1,不然返回0
* 在“查看其二进制位中有多少个”时使用了一个小技巧
* 使用n &= (n-1)能够使得n中的减小一个(具体原理你们能够本身思考)
*/
int power_of_two( int n)
{
     if( n <= 0 )
         return 0;
     return (n & (n-1)) == 0;
}

/* 函数load_texture
* 读取一个BMP文件做为纹理
* 若是失败,返回0,若是成功,返回纹理编号
*/
GLuint load_texture( const  char* file_name)
{
     GLint width, height, total_bytes;
     GLubyte* pixels = 0;
     GLuint last_texture_ID, texture_ID = 0;

     // 打开文件,若是失败,返回
     FILE* pFile =  fopen(file_name,  "rb");
     if( pFile == 0 )
         return 0;

     // 读取文件中图象的宽度和高度
     fseek(pFile, 0x0012, SEEK_SET);
     fread(&width, 4, 1, pFile);
     fread(&height, 4, 1, pFile);
     fseek(pFile, BMP_Header_Length, SEEK_SET);

     // 计算每行像素所占字节数,并根据此数据计算总像素字节数
     {
         GLint line_bytes = width * 3;
         while( line_bytes % 4 != 0 )
             ++line_bytes;
         total_bytes = line_bytes * height;
     }

     // 根据总像素字节数分配内存
     pixels = (GLubyte*) malloc(total_bytes);
     if( pixels == 0 )
     {
         fclose(pFile);
         return 0;
     }

     // 读取像素数据
     iffread(pixels, total_bytes, 1, pFile) <= 0 )
     {
         free(pixels);
         fclose(pFile);
         return 0;
     }

     // 在旧版本的OpenGL中
     // 若是图象的宽度和高度不是的整数次方,则须要进行缩放
     // 这里并无检查OpenGL版本,出于对版本兼容性的考虑,按旧版本处理
     // 另外,不管是旧版本仍是新版本,
     // 当图象的宽度和高度超过当前OpenGL实现所支持的最大值时,也要进行缩放
     {
         GLint  max;
         glGetIntegerv(GL_MAX_TEXTURE_SIZE, & max);
         if( !power_of_two(width)
          || !power_of_two(height)
          || width >  max
          || height >  max )
         {
             const GLint new_width = 256;
             const GLint new_height = 256; // 规定缩放后新的大小为边长的正方形
             GLint new_line_bytes, new_total_bytes;
             GLubyte* new_pixels = 0;

             // 计算每行须要的字节数和总字节数
             new_line_bytes = new_width * 3;
             while( new_line_bytes % 4 != 0 )
                 ++new_line_bytes;
             new_total_bytes = new_line_bytes * new_height;

             // 分配内存
             new_pixels = (GLubyte*) malloc(new_total_bytes);
             if( new_pixels == 0 )
             {
                 free(pixels);
                 fclose(pFile);
                 return 0;
             }

             // 进行像素缩放
             gluScaleImage(GL_RGB,
                 width, height, GL_UNSIGNED_BYTE, pixels,
                 new_width, new_height, GL_UNSIGNED_BYTE, new_pixels);

             // 释放原来的像素数据,把pixels指向新的像素数据,并从新设置width和height
             free(pixels);
             pixels = new_pixels;
             width = new_width;
             height = new_height;
         }
     }

     // 分配一个新的纹理编号
     glGenTextures(1, &texture_ID);
     if( texture_ID == 0 )
     {
         free(pixels);
         fclose(pFile);
         return 0;
     }

     // 绑定新的纹理,载入纹理并设置纹理参数
     // 在绑定前,先得到原来绑定的纹理编号,以便在最后进行恢复
     glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture_ID);
     glBindTexture(GL_TEXTURE_2D, texture_ID);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
     glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
         GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);
     glBindTexture(GL_TEXTURE_2D, last_texture_ID);

     // 以前为pixels分配的内存可在使用glTexImage2D之后释放
     // 由于此时像素数据已经被OpenGL另行保存了一份(可能被保存到专门的图形硬件中)
     free(pixels);
     return texture_ID;
}

第三段代码是关于显示的部分,以及main函数。注意,咱们只在main函数中读取了两幅纹理,并把它们保存在各自的纹理对象中,之后就不再载入纹理。每次绘制时使用glBindTexture在不一样的纹理对象中切换。另外,咱们使用了超过1.0的纹理坐标,因为GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T参数都被设置为GL_REPEAT,因此获得的效果就是纹理像素的重复,有点向地板砖的花纹那样。读者能够试着修改“墙”的纹理坐标,将5.0修改成10.0,看看效果有什么变化。

/* 两个纹理对象的编号
*/
GLuint texGround;
GLuint texWall;

void display( void)
{
     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 设置视角
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(75, 1, 1, 21);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(1, 5, 5, 0, 0, 0, 0, 0, 1);

     // 使用“地”纹理绘制土地
     glBindTexture(GL_TEXTURE_2D, texGround);
     glBegin(GL_QUADS);
         glTexCoord2f(0.0f, 0.0f); glVertex3f(-8.0f, -8.0f, 0.0f);
         glTexCoord2f(0.0f, 5.0f); glVertex3f(-8.0f, 8.0f, 0.0f);
         glTexCoord2f(5.0f, 5.0f); glVertex3f(8.0f, 8.0f, 0.0f);
         glTexCoord2f(5.0f, 0.0f); glVertex3f(8.0f, -8.0f, 0.0f);
     glEnd();
     // 使用“墙”纹理绘制栅栏
     glBindTexture(GL_TEXTURE_2D, texWall);
     glBegin(GL_QUADS);
         glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
         glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
     glEnd();

     // 旋转后再绘制一个
     glRotatef(-90, 0, 0, 1);
     glBegin(GL_QUADS);
         glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
         glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
     glEnd();

     // 交换缓冲区,并保存像素数据到文件
     glutSwapBuffers();
     grab();
}

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

     // 在这里作一些初始化
     glEnable(GL_DEPTH_TEST);
     glEnable(GL_TEXTURE_2D);
     texGround = load_texture( "ground.bmp");
     texWall = load_texture( "wall.bmp");

     // 开始显示
     glutMainLoop();

     return 0;
}

小结:
本课介绍了OpenGL纹理的入门知识。
利用纹理能够进行比glReadPixels和glDrawPixels更复杂的像素绘制,所以能够实现不少精彩的效果。
本课只涉及了二维纹理。OpenGL还支持一维和三维纹理,其原理是相似的。
在使用纹理前,要启用纹理。而且,还须要将像素数据载入到纹理中。注意纹理的宽度和高度,目前不少OpenGL的实现都还要求其值为2的整数次方,若是纹理图象自己并不知足这个条件,能够使用gluScaleImage函数来进行缩放。为了正确的使用纹理,须要设置纹理参数。
载入纹理所须要的系统开销是比较大的,应该尽量减小载入纹理的次数。若是程序中只使用一幅纹理,则只在第一次使用前载入,之后没必要从新载入。若是程序中要使用多幅纹理,不该该反复载入它们,而应该将每一个纹理都用一个纹理对象来保存,并使用glBindTextures在各个纹理之间进行切换。
本课还给出了一个程序(到目前为止,它是这个OpenGL教程系列中所给出的程序中最长的)。该程序演示了纹理的基本使用方法,本课程涉及到的几乎全部内容都被包括其中,这是对本课中文字说明的一个补充。若是读者有什么不明白的地方,也能够以这个程序做为参考。



OpenGL入门学习[十二]

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

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

一、剪裁测试
剪裁测试用于限制绘制区域。咱们能够指定一个矩形的剪裁窗口,当启用剪裁测试后,只有在这个窗口以内的像素才能被绘制,其它像素则会被丢弃。换句话说,不管怎么绘制,剪裁窗口之外的像素将不会被修改。
有的朋友可能玩过《魔兽争霸3》这款游戏。游戏时若是选中一个士兵,则画面下方的一个方框内就会出现该士兵的头像。为了保证该头像不管如何绘制都不会越界而覆盖到外面的像素,就能够使用剪裁测试。

能够经过下面的代码来启用或禁用剪裁测试:

glEnable(GL_SCISSOR_TEST);   // 启用剪裁测试
glDisable(GL_SCISSOR_TEST); // 禁用剪裁测试



能够经过下面的代码来指定一个位置在(x, y),宽度为width,高度为height的剪裁窗口。

glScissor(x, y, width, height);


注意,OpenGL窗口坐标是以左下角为(0, 0),右上角为(width, height)的,这与Windows系统窗口有所不一样。

还有一种方法能够保证像素只绘制到某一个特定的矩形区域内,这就是视口变换(在第五课第3节中有介绍)。但视口变换和剪裁测试是不一样的。视口变换是将全部内容缩放到合适的大小后,放到一个矩形的区域内;而剪裁测试不会进行缩放,超出矩形范围的像素直接忽略掉。

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

二、Alpha测试
在前面的课程中,咱们知道像素的Alpha值能够用于混合操做。其实Alpha值还有一个用途,这就是Alpha测试。当每一个像素即将绘制时,若是启动了Alpha测试,OpenGL会检查像素的Alpha值,只有Alpha值知足条件的像素才会进行绘制(严格的说,知足条件的像素会经过本项测试,进行下一种测试,只有全部测试都经过,才能进行绘制),不知足条件的则不进行绘制。这个“条件”能够是:始终经过(默认状况)、始终不经过、大于设定值则经过、小于设定值则经过、等于设定值则经过、大于等于设定值则经过、小于等于设定值则经过、不等于设定值则经过。
若是咱们须要绘制一幅图片,而这幅图片的某些部分又是透明的(想象一下,你先绘制一幅相片,而后绘制一个相框,则相框这幅图片有不少地方都是透明的,这样就能够透过相框看到下面的照片),这时能够使用Alpha测试。将图片中全部须要透明的地方的Alpha值设置为0.0,不须要透明的地方Alpha值设置为1.0,而后设置Alpha测试的经过条件为:“大于0.5则经过”,这样便能达到目的。固然也能够设置须要透明的地方Alpha值为1.0,不须要透明的地方Alpha值设置为0.0,而后设置条件为“小于0.5则经过”。Alpha测试的设置方式每每不仅一种,能够根据我的喜爱和实际状况须要进行选择。

能够经过下面的代码来启用或禁用Alpha测试:

glEnable(GL_ALPHA_TEST);   // 启用Alpha测试
glDisable(GL_ALPHA_TEST); // 禁用Alpha测试



能够经过下面的代码来设置Alpha测试条件为“大于0.5则经过”:

glAlphaFunc(GL_GREATER, 0.5f);



该函数的第二个参数表示设定值,用于进行比较。第一个参数是比较方式,除了GL_LESS(小于则经过)外,还能够选择:
GL_ALWAYS(始终经过),
GL_NEVER(始终不经过),
GL_LESS(小于则经过),
GL_LEQUAL(小于等于则经过),
GL_EQUAL(等于则经过),
GL_GEQUAL(大于等于则经过),
GL_NOTEQUAL(不等于则经过)。

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

如今咱们来看一个实际例子。一幅照片图片,一幅相框图片,如何将它们组合在一块儿呢?为了简单起见,咱们使用前面两课一直使用的24位BMP文件来做为图片格式。(由于发布到网络上,为了节约容量,我所发布的是JPG格式。你们下载后能够用Windows XP自带的画图工具打开,并另存为24位BMP格式)
http://blog.programfan.com/upfile/200710/2007100711109.jpghttp://blog.programfan.com/upfile/200710/20071007111014.jpg
注:第一幅图片是著名网络游戏《魔兽世界》的一幅桌面背景,用在这里但愿没有涉及版权问题。若是有什么不妥,请及时指出,我会当即更换。

在24位的BMP文件格式中,BGR三种颜色各占8位,没有保存Alpha值,所以没法直接使用Alpha测试。注意到相框那幅图片中,全部须要透明的位置都是白色,因此咱们在程序中设置全部白色(或很接近白色)的像素Alpha值为0.0,设置其它像素Alpha值为1.0,而后设置Alpha测试的条件为“大于0.5则经过”便可。这种使用某种特殊颜色来表明透明颜色的技术,有时又被成为Color Key技术。
利用前面第11课的一段代码,将图片读取为纹理,而后利用下面这个函数来设置“当前纹理”中每个像素的Alpha值。

/* 将当前纹理BGR格式转换为BGRA格式
* 纹理中像素的RGB值若是与指定rgb相差不超过absolute,则将Alpha设置为0.0,不然设置为1.0
*/
void texture_colorkey(GLubyte r, GLubyte g, GLubyte b, GLubyte absolute)
{
     GLint width, height;
     GLubyte* pixels = 0;

     // 得到纹理的大小信息
     glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
     glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);

     // 分配空间并得到纹理像素
     pixels = (GLubyte*) malloc(width*height*4);
     if( pixels == 0 )
         return;
     glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);

     // 修改像素中的Alpha值
     // 其中pixels[i*4], pixels[i*4+1], pixels[i*4+2], pixels[i*4+3]
     //    分别表示第i个像素的蓝、绿、红、Alpha四种份量,0表示最小,255表示最大
     {
         GLint i;
         GLint  count = width * height;
         for(i=0; i< count; ++i)
         {
             ifabs(pixels[i*4] - b) <= absolute
              &&  abs(pixels[i*4+1] - g) <= absolute
              &&  abs(pixels[i*4+2] - r) <= absolute )
                 pixels[i*4+3] = 0;
             else
                 pixels[i*4+3] = 255;
         }
     }

     // 将修改后的像素从新设置到纹理中,释放内存
     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
         GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);
     free(pixels);
}



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

 

有了纹理后,咱们开启纹理,指定合适的纹理坐标并绘制一个矩形,这样就能够在屏幕上将图片绘制出来。咱们先绘制相片的纹理,再绘制相框的纹理。程序代码以下:

void display( void)
{
     static  int initialized    = 0;
     static GLuint texWindow   = 0;
     static GLuint texPicture = 0;

     // 执行初始化操做,包括:读取相片,读取相框,将相框由BGR颜色转换为BGRA,启用二维纹理
     if( !initialized )
     {
         texPicture = load_texture( "pic.bmp");
         texWindow   = load_texture( "window.bmp");
         glBindTexture(GL_TEXTURE_2D, texWindow);
         texture_colorkey(255, 255, 255, 10);

         glEnable(GL_TEXTURE_2D);

         initialized = 1;
     }

     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT);

     // 绘制相片,此时不须要进行Alpha测试,全部的像素都进行绘制
     glBindTexture(GL_TEXTURE_2D, texPicture);
     glDisable(GL_ALPHA_TEST);
     glBegin(GL_QUADS);
         glTexCoord2f(0, 0);      glVertex2f(-1.0f, -1.0f);
         glTexCoord2f(0, 1);      glVertex2f(-1.0f,   1.0f);
         glTexCoord2f(1, 1);      glVertex2f( 1.0f,   1.0f);
         glTexCoord2f(1, 0);      glVertex2f( 1.0f, -1.0f);
     glEnd();

     // 绘制相框,此时进行Alpha测试,只绘制不透明部分的像素
     glBindTexture(GL_TEXTURE_2D, texWindow);
     glEnable(GL_ALPHA_TEST);
     glAlphaFunc(GL_GREATER, 0.5f);
     glBegin(GL_QUADS);
         glTexCoord2f(0, 0);      glVertex2f(-1.0f, -1.0f);
         glTexCoord2f(0, 1);      glVertex2f(-1.0f,   1.0f);
         glTexCoord2f(1, 1);      glVertex2f( 1.0f,   1.0f);
         glTexCoord2f(1, 0);      glVertex2f( 1.0f, -1.0f);
     glEnd();

     // 交换缓冲
     glutSwapBuffers();
}


其中:load_texture函数是从第11课中照搬过来的(该函数还使用了一个power_of_two函数,一个BMP_Header_Length常数,一样照搬),无需进行修改。main函数跟其它课程的基本相同,再也不重复。
程序运行后,会发现相框与相片的衔接有些不天然,这是由于相框某些边缘部分虽然肉眼看上去是白色,但其实RGB值与纯白色相差并很多,所以程序计算其Alpha值时认为其不须要透明。解决办法是仔细处理相框中的每一个像素,在须要透明的地方涂上纯白色,这也许是一件很须要耐心的工做。

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

 

你们可能会想:前面咱们学习过混合操做,混合能够实现半透明,天然也能够经过设定实现全透明。也就是说,Alpha测试能够实现的效果几乎均可以经过OpenGL混合功能来实现。那么为何还须要一个Alpha测试呢?答案就是,这与性能相关。Alpha测试只要简单的比较大小就能够获得最终结果,而混合操做通常须要进行乘法运算,性能有所降低。另外,OpenGL测试的顺序是:剪裁测试、Alpha测试、模板测试、深度测试。若是某项测试不经过,则不会进行下一步,而只有全部测试都经过的状况下才会执行混合操做。所以,在使用Alpha测试的状况下,透明的像素就不须要通过模板测试和深度测试了;而若是使用混合操做,即便透明的像素也须要进行模板测试和深度测试,性能会有所降低。还有一点:对于那些“透明”的像素来讲,若是使用Alpha测试,则“透明”的像素不会经过测试,所以像素的深度值不会被修改;而使用混合操做时,虽然像素的颜色没有被修改,但它的深度值则有可能被修改掉了。
所以,若是全部的像素都是“透明”或“不透明”,没有“半透明”时,应该尽可能采用Alpha测试而不是采用混合操做。当须要绘制半透明像素时,才采用混合操做。

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

 

三、模板测试
模板测试是全部OpenGL测试中比较复杂的一种。

首先,模板测试须要一个模板缓冲区,这个缓冲区是在初始化OpenGL时指定的。若是使用GLUT工具包,能够在调用glutInitDisplayMode函数时在参数中加上GLUT_STENCIL,例如:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_STENCIL);


在Windows操做系统中,即便没有明确要求使用模板缓冲区,有时候也会分配模板缓冲区。但为了保证程序的通用性,最好仍是明确指定使用模板缓冲区。若是确实没有分配模板缓冲区,则全部进行模板测试的像素所有都会经过测试。

经过glEnable/glDisable能够启用或禁用模板测试。

glEnable(GL_STENCIL_TEST);   // 启用模板测试
glDisable(GL_STENCIL_TEST); // 禁用模板测试



OpenGL在模板缓冲区中为每一个像素保存了一个“模板值”,当像素须要进行模板测试时,将设定的模板参考值与该像素的“模板值”进行比较,符合条件的经过测试,不符合条件的则被丢弃,不进行绘制。
条件的设置与Alpha测试中的条件设置类似。但注意Alpha测试中是用浮点数来进行比较,而模板测试则是用整数来进行比较。比较也有八种状况:始终经过、始终不经过、大于则经过、小于则经过、大于等于则经过、小于等于则经过、等于则经过、不等于则经过。

glStencilFunc(GL_LESS, 3, mask);


这段代码设置模板测试的条件为:“小于3则经过”。glStencilFunc的前两个参数意义与glAlphaFunc的两个参数相似,第三个参数的意义为:若是进行比较,则只比较mask中二进制为1的位。例如,某个像素模板值为5(二进制101),而mask的二进制值为00000011,由于只比较最后两位,5的最后两位为01,实际上是小于3的,所以会经过测试。

如何设置像素的“模板值”呢?glClear函数能够将全部像素的模板值复位。代码以下:

glClear(GL_STENCIL_BUFFER_BIT);


能够同时复位颜色值和模板值:

glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);


正如能够使用glClearColor函数来指定清空屏幕后的颜色那样,也能够使用glClearStencil函数来指定复位后的“模板值”。

每一个像素的“模板值”会根据模板测试的结果和深度测试的结果而进行改变。

glStencilOp(fail, zfail, zpass);


该函数指定了三种状况下“模板值”该如何变化。第一个参数表示模板测试未经过时该如何变化;第二个参数表示模板测试经过,但深度测试未经过时该如何变化;第三个参数表示模板测试和深度测试均经过时该如何变化。若是没有起用模板测试,则认为模板测试老是经过;若是没有启用深度测试,则认为深度测试老是经过)
变化能够是:
GL_KEEP(不改变,这也是默认值),
GL_ZERO(回零),
GL_REPLACE(使用测试条件中的设定值来代替当前模板值),
GL_INCR(增长1,但若是已是最大值,则保持不变),
GL_INCR_WRAP(增长1,但若是已是最大值,则从零从新开始),
GL_DECR(减小1,但若是已是零,则保持不变),
GL_DECR_WRAP(减小1,但若是已是零,则从新设置为最大值),
GL_INVERT(按位取反)。

在新版本的OpenGL中,容许为多边形的正面和背面使用不一样的模板测试条件和模板值改变方式,因而就有了glStencilFuncSeparate函数和glStencilOpSeparate函数。这两个函数分别与glStencilFunc和glStencilOp相似,只在最前面多了一个参数face,用于指定当前设置的是哪一个面。能够选择GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。

注意:模板缓冲区与深度缓冲区有一点不一样。不管是否启用深度测试,当有像素被绘制时,总会从新设置该像素的深度值(除非设置glDepthMask(GL_FALSE);)。而模板测试若是不启用,则像素的模板值会保持不变,只有启用模板测试时才有可能修改像素的模板值。(这一结论是我本身的实验得出的,暂时没发现什么资料上是这样写。若是有不正确的地方,欢迎指正)
另外,模板测试虽然是从OpenGL 1.0就开始提供的功能,可是对于我的计算机而言,硬件实现模板测试的彷佛并很少,不少计算机系统直接使用CPU运算来完成模板测试。所以在一些老的显卡,或者是多数集成显卡上,大量而频繁的使用模板测试可能形成程序运行效率低下。即便是当前配置比较高端的我的计算机,也尽可能不要使用glStencilFuncSeparate和glStencilOpSeparate函数。

从前面所讲能够知道,使用剪裁测试能够把绘制区域限制在一个矩形的区域内。但若是须要把绘制区域限制在一个不规则的区域内,则须要使用模板测试。
例如:绘制一个湖泊,以及周围的树木,而后绘制树木在湖泊中的倒影。为了保证倒影被正确的限制在湖泊表面,能够使用模板测试。具体的步骤以下:
(1) 关闭模板测试,绘制地面和树木。
(2) 开启模板测试,使用glClear设置全部像素的模板值为0。
(3) 设置glStencilFunc(GL_ALWAYS, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);绘制湖泊水面。这样一来,湖泊水面的像素的“模板值”为1,而其它地方像素的“模板值”为0。
(4) 设置glStencilFunc(GL_EQUAL, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);绘制倒影。这样一来,只有“模板值”为1的像素才会被绘制,所以只有“水面”的像素才有可能被倒影的像素替换,而其它像素则保持不变。

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

 

咱们仍然来看一个实际的例子。这是一个比较简单的场景:空间中有一个球体,一个平面镜。咱们站在某个特殊的观察点,能够看到球体在平面镜中的镜像,而且镜像处于平面镜的边缘,有一部分由于平面镜大小的限制,而没法显示出来。整个场景的效果以下图:
http://blog.programfan.com/upfile/200710/20071007111019.jpg

绘制这个场景的思路跟前面提到的湖面倒影是接近的。
假设平面镜所在的平面正好是X轴和Y轴所肯定的平面,则球体和它在平面镜中的镜像是关于这个平面对称的。咱们用一个draw_sphere函数来绘制球体,先调用该函数以绘制球体自己,而后调用glScalef(1.0f, 1.0f, -1.0f); 再调用draw_sphere函数,就能够绘制球体的镜像。
另外须要注意的地方就是:由于是绘制三维的场景,咱们开启了深度测试。可是站在观察者的位置,球体的镜像实际上是在平面镜的“背后”,也就是说,若是按照常规的方式绘制,平面镜会把镜像覆盖掉,这不是咱们想要的效果。解决办法就是:设置深度缓冲区为只读,绘制平面镜,而后设置深度缓冲区为可写的状态,绘制平面镜“背后”的镜像。
有的朋友可能会问:若是在绘制镜像的时候关闭深度测试,那镜像不就不会被平面镜遮挡了吗?为何还要开启深度测试,又须要把深度缓冲区设置为只读呢?实际状况是:虽然关闭深度测试确实可让镜像不被平面镜遮挡,可是镜像自己会出现若干问题。咱们看到的镜像是一个球体,但实际上这个球体是由不少的多边形所组成的,这些多边形有的表明了咱们所能看到的“正面”,有的则表明了咱们不能看到的“背面”。若是关闭深度测试,而有的“背面”多边形又比“正面”多边形先绘制,就会形成球体的背面反而把正面挡住了,这不是咱们想要的效果。为了确保正面能够挡住背面,应该开启深度测试。
绘制部分的代码以下:

void draw_sphere()
{
     // 设置光源
     glEnable(GL_LIGHTING);
     glEnable(GL_LIGHT0);
     {
         GLfloat
             pos[]      = {5.0f, 5.0f, 0.0f, 1.0f},
             ambient[] = {0.0f, 0.0f, 1.0f, 1.0f};
         glLightfv(GL_LIGHT0, GL_POSITION, pos);
         glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
     }

     // 绘制一个球体
     glColor3f(1, 0, 0);
     glPushMatrix();
     glTranslatef(0, 0, 2);
     glutSolidSphere(0.5, 20, 20);
     glPopMatrix();
}

void display( void)
{
     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 设置观察点
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(60, 1, 5, 25);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(5, 0, 6.5, 0, 0, 0, 0, 1, 0);

     glEnable(GL_DEPTH_TEST);

     // 绘制球体
     glDisable(GL_STENCIL_TEST);
     draw_sphere();

     // 绘制一个平面镜。在绘制的同时注意设置模板缓冲。
     // 另外,为了保证平面镜以后的镜像可以正确绘制,在绘制平面镜时须要将深度缓冲区设置为只读的。
     // 在绘制时暂时关闭光照效果
     glClearStencil(0);
     glClear(GL_STENCIL_BUFFER_BIT);
     glStencilFunc(GL_ALWAYS, 1, 0xFF);
     glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
     glEnable(GL_STENCIL_TEST);

     glDisable(GL_LIGHTING);
     glColor3f(0.5f, 0.5f, 0.5f);
     glDepthMask(GL_FALSE);
     glRectf(-1.5f, -1.5f, 1.5f, 1.5f);
     glDepthMask(GL_TRUE);

     // 绘制一个与先前球体关于平面镜对称的球体,注意光源的位置也要发生对称改变
     // 由于平面镜是在X轴和Y轴所肯定的平面,因此只要Z坐标取反便可实现对称
     // 为了保证球体的绘制范围被限制在平面镜内部,使用模板测试
     glStencilFunc(GL_EQUAL, 1, 0xFF);
     glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
     glScalef(1.0f, 1.0f, -1.0f);
     draw_sphere();

     // 交换缓冲
     glutSwapBuffers();

     // 截图
     grab();
}



其中display函数的末尾调用了一个grab函数,它保存当前的图象到一个BMP文件。这个函数原本是在第十课和第十一课中都有所使用的。可是我发现它有一个bug,如今进行了修改:在函数最开头的部分加上一句:glReadBuffer(GL_FRONT);便可。注意这个函数最好是在绘制完毕后(若是是使用双缓冲,则应该在交换缓冲后)当即调用。

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

 

你们可能会有这样的感受:模板测试的设置是如此复杂,它能够实现的功能应该不少,确定不止这样一个“限制像素的绘制范围”。事实上也是如此,不过如今咱们暂时只讲这些。

其实,若是不须要绘制半透明效果,有时候能够用混合功能来代替模板测试。就绘制镜像这个例子来讲,能够采用下面的步骤:
(1) 清除屏幕,在glClearColor中设置合适的值确保清除屏幕后像素的Alpha值为0.0
(2) 关闭混合功能,绘制球体自己,设置合适的颜色(或者光照与材质)以确保全部被绘制的像素的Alpha值为0.0
(3) 绘制平面镜,设置合适的颜色(或者光照与材质)以确保全部被绘制的像素的Alpha值为1.0
(4) 启用混合功能,用GL_DST_ALPHA做为源因子,GL_ONE_MINUS_DST_ALPHA做为目标因子,这样就实现了只有原来Alpha为1.0的像素才能被修改,而原来Alpha为0.0的像素则保持不变。这时再绘制镜像物体,注意确保全部被绘制的像素的Alpha值为1.0。
在有的OpenGL实现中,模板测试是软件实现的,而混合功能是硬件实现的,这时候能够考虑这样的代替方法以提升运行效率。可是并不是全部的模板测试均可以用混合功能来代替,而且这样的代替显得不天然,复杂并且容易出错。
另外始终注意:使用混合来模拟时,即便某个像素原来的Alpha值为0.0,以至于在绘制后其颜色不会有任何变化,可是这个像素的深度值有可能会被修改,而若是是使用模板测试,没有经过测试的像素其深度值不会发生任何变化。并且,模板测试和混合功能中,像素模板值的修改方式是不同的。

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

 

四、深度测试
在本课的开头,已经简单的叙述了深度测试。这里是完整的内容。

深度测试须要深度缓冲区,跟模板测试须要模板缓冲区是相似的。若是使用GLUT工具包,能够在调用glutInitDisplayMode函数时在参数中加上GLUT_DEPTH,这样来明确指定要求使用深度缓冲区。
深度测试和模板测试的实现原理很相似,都是在一个缓冲区保存像素的某个值,当须要进行测试时,将保存的值与另外一个值进行比较,以肯定是否经过测试。二者的区别在于:模板测试是设定一个值,在测试时用这个设定值与像素的“模板值”进行比较,而深度测试是根据顶点的空间坐标计算出深度,用这个深度与像素的“深度值”进行比较。也就是说,模板测试须要指定一个值做为比较参考,而深度测试中,这个比较用的参考值是OpenGL根据空间坐标自动计算的。

经过glEnable/glDisable函数能够启用或禁用深度测试。
glEnable(GL_DEPTH_TEST);   // 启用深度测试
glDisable(GL_DEPTH_TEST); // 禁用深度测试

至于经过测试的条件,一样有八种,与Alpha测试中的条件设置相同。条件设置是经过glDepthFunc函数完成的,默认值是GL_LESS。
glDepthFunc(GL_LESS);

与模板测试相比,深度测试的应用要频繁得多。几乎全部的三维场景绘制都使用了深度测试。正由于这样,几乎全部的OpenGL实现都对深度测试提供了硬件支持,因此虽然二者的实现原理相似,但深度测试极可能会比模板测试快得多。固然了,两种测试在应用上不多有交集,通常不会出现使用一种测试去代替另外一种测试的状况。

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

 

小结:
本次课程介绍了OpenGL所提供的四种测试,分别是剪裁测试、Alpha测试、模板测试、深度测试。OpenGL会对每一个即将绘制的像素进行以上四种测试,每一个像素只有经过一项测试后才会进入下一项测试,而只有经过全部测试的像素才会被绘制,没有经过测试的像素会被丢弃掉,不进行绘制。每种测试均可以单独的开启或者关闭,若是某项测试被关闭,则认为全部像素均可以顺利经过该项测试。
剪裁测试是指:只有位于指定矩形内部的像素才能经过测试。
Alpha测试是指:只有Alpha值与设定值相比较,知足特定关系条件的像素才能经过测试。
模板测试是指:只有像素模板值与设定值相比较,知足特定关系条件的像素才能经过测试。
深度测试是指:只有像素深度值与新的深度值比较,知足特定关系条件的像素才能经过测试。
上面所说的特定关系条件能够是大于、小于、等于、大于等于、小于等于、不等于、始终经过、始终不经过这八种。
模板测试须要模板缓冲区,深度测试须要深度缓冲区。这些缓冲区都是在初始化OpenGL时指定的。若是使用GLUT工具包,则能够在glutInitDisplayMode函数中指定。不管是否开启深度测试,OpenGL在像素被绘制时都会尝试修改像素的深度值;而只有开启模板测试时,OpenGL才会尝试修改像素的模板值,模板测试被关闭时,OpenGL在像素被绘制时也不会修改像素的模板值。
利用这些测试操做能够控制像素被绘制或不被绘制,从而实现一些特殊效果。利用混合功能能够实现半透明,经过设置也能够实现彻底透明,于是能够模拟像素颜色的绘制或不绘制。但注意,这里仅仅是颜色的模拟。OpenGL能够为像素保存颜色、深度值和模板值,利用混合实现透明时,像素颜色不发生变化,但深度值则会可能变化,模板值受glStencilFunc函数中第三个参数影响;利用测试操做实现透明时,像素颜色不发生变化,深度值也不发生变化,模板值受glStencilFunc函数中前两个参数影响。
此外,修正了第十课、第十一课中的一个函数中的bug。在grab函数中,应该在最开头加上一句glReadBuffer(GL_FRONT);以保证读取到的内容正好就是显示的内容。

由于论坛支持附件了,我会把程序源代码和所使用的图片上传到附件里,方便你们下载。

=====================   第十二课 完   =====================
=====================TO BE CONTINUED=====================


OpenGL入门学习[十三]



前一段时间里,论坛有位朋友问什么是状态机。按个人理解,状态机就是一种存在于理论中的机器,它具备如下的特色:

1. 它有记忆的能力,可以记住本身当前的状态。

2. 它能够接收输入,根据输入的内容和本身的状态,修改本身的状态,而且能够获得输出。

3. 当它进入某个特殊的状态(停机状态)的时候,它再也不接收输入,中止工做。

理论提及来很抽象,但其实是很好理解的。

首先,从本质上讲,咱们如今的电脑就是典型的状态机。能够对照理解:

1. 电脑的存储器(内存、硬盘等等),能够记住电脑本身当前的状态(当前安装在电脑中的软件、保存在电脑中的数据,其实都是二进制的值,都属于当前的状态)。

2. 电脑的输入设备接收输入(键盘输入、鼠标输入、文件输入),根据输入的内容和本身的状态(主要指能够运行的程序代码),修改本身的状态(修改内存中的值),而且能够获得输出(将结果显示到屏幕)。

3. 当它进入某个特殊的状态(关机状态)的时候,它再也不接收输入,中止工做。

OpenGL也能够当作这样的一种机器。让咱们先对照理解一下:

1. OpenGL能够记录本身的状态(好比:当前所使用的颜色、是否开启了混合功能,等等,这些都是要记录的)

2. OpenGL能够接收输入(当咱们调用OpenGL函数的时候,实际上能够当作OpenGL在接收咱们的输入),根据输入的内容和本身的状态,修改本身的状态,而且能够获得输出(好比咱们调用glColor3f,则OpenGL接收到这个输入后会修改本身的“当前颜色”这个状态;咱们调用glRectf,则OpenGL会输出一个矩形)

3. OpenGL能够进入中止状态,再也不接收输入。这个可能在咱们的程序中表现得不太明显,不过在程序退出前,OpenGL总会先中止工做的。

仍是没理解?呵呵,看来这真不是个好的开始呀,可贵等了这么久,好不容易教程有更新了,怎么如此的难懂啊??不要紧,实在没理解,咱就不理解它了。接着往下看。

为何我要提到“状态机”这个枯燥的、晦涩的概念呢?其实它能够帮助咱们理解一些东西。

好比我在前面的教程里面,常常说:

能够使用glColor*函数来选择一种颜色,之后绘制的全部物体都是这种颜色,除非再次使用glColor*函数从新设定。

能够使用glTexCoord*函数来设置一个纹理坐标,之后绘制的全部物体都是采用这种纹理坐标,除非再次使用glTexCoord*函数从新设置。

能够使用glBlendFunc函数来指定混合功能的源因子和目标因子,之后绘制的全部物体都是采用这个源因子和目标因子,除非再次使用glBlendFunc函数从新指定。

能够使用glLight*函数来指定光源的位置、颜色,之后绘制的全部物体都是采用这个光源的位置、颜色,除非再次使用glBlendFunc函数从新指定。

……

呵呵,很繁,是吧?“状态机”能够简化这个描述。

OpenGL是一个状态机,它保持自身的状态,除非用户输入一条命令让它改变状态。

颜色、纹理坐标、源因子和目标因子、光源的各类参数,等等,这些都是状态,因此这一句话就包含了上面叙述的全部内容。

此外,“是否启用了光照”、“是否启用了纹理”、“是否启用了混合”、“是否启用了深度测试”等等,这些也都是状态,也符合上面的描述:OpenGL会保持状态,除非咱们调用OpenGL函数来改变它。

取得OpenGL的当前状态

OpenGL保存了本身的状态,咱们能够经过一些函数来取得这些状态。

首先来讲一些启用/禁用的状态。

咱们经过glEnable来启用状态,经过glDisable来禁用它们。例如:

glEnable(GL_DEPTH_TEST);

glEnable(GL_BLEND);

glEnable(GL_CULL_FACE);

glEnable(GL_LIGHTING);

glEnable(GL_TEXTURE_2D);

能够用glIsEnabled函数来检测这些状态是否被开启。例如:

glIsEnabled(GL_DEPTH_TEST);

glIsEnabled(GL_BLEND);

glIsEnabled(GL_CULL_FACE);

glIsEnabled(GL_LIGHTING);

glIsEnabled(GL_TEXTURE_2D);

若是状态是开启的,则glIsEnabled函数返回GL_TRUE(这是一个不为零的常量,通常被定义为1);不然返回GL_FALSE(这是一个常量,其值为零)

咱们能够在程序里面写:

if( glIsEnabled(GL_BLEND) ) {

     // 当前开启了混合功能

} else {

     // 当前没有开启混合功能

}

再看其它类型的状态。

好比当前颜色,其值是四个浮点数,当前设置的直线宽度,其值是一个浮点数,当前的视口(Viewport,参见第五课),其值是四个整数。

为了取得整数类型、浮点数类型的状态,OpenGL提供了glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev这四个函数。调用函数时,指定须要获得的状态的名称,以及须要将状态值存放到的位置(一个指针),则这四个函数能够把状态值存放到指针所值位置。例如:

// 取得当前的直线宽度

GLfloat lw;

glGetFloatv(GL_LINE_WIDTH, &lw);

// 取得当前的颜色

GLfloat cc[4];

glGetFloatv(GL_CURRENT_COLOR, cc);

// 取得当前的视口

GLint viewport[4];

glGetIntegerv(GL_VIEWPORT, viewport);

说明:

1. 注意元素的个数。好比GL_LINE_WIDTH状态只有一个值,而GL_CURRENT_COLOR有四个值。应该当心的定义变量或者数组,避免下标越界。

2. 使用四个不一样的函数,同一种状态也能够返回为不一样类型的值。好比要获得当前的颜色,通常能够返回GLfloat类型或者GLdouble类型。代码以下:

GLfloat cc[4];

GLdouble dcc[4];

glGetFloatv(GL_CURRENT_COLOR, cc);

glGetDoublev(GL_CURRENT_COLOR, dcc);

glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev这四个函数能够获得OpenGL中多数的状态,可是还有一些状态不便用这四个函数来取得。好比光源的状态,由于可能有多个光源,因此不可能使用相似glGetFloatv(GL_LIGHT_POSITION, pos);这样的方法来获得光源位置。为了解决这个问题,OpenGL专门提供了glGetLight*系列函数,来取得光源的状态。

相似的,还有glGetMaterial*, glGetTexParameter*等,每一个函数都有本身的适用范围。

设置OpenGL状态

呵呵,读者可能会有疑问。既然有getXXX这样的函数来取得OpenGL的状态,那么为何没有setXXX这样的函数来设置OpenGL状态呢?

答案很简单,由于OpenGL已经提供了大量的函数来设置状态了:glColor*, glMaterial*, glEnable, glDisable, 等等,大多数OpenGL函数都是用来设置OpenGL状态的,所以不须要再设计一个setXXX函数来设置OpenGL状态。

从“状态机”的角度来看。状态机根据输入来修改本身的状态,而不是由外界直接修改本身的状态。因此不设置setXXX这样的函数,也是很合理的。

OpenGL工做流程

教程都放到第十三课了,可是我一直没有对“工做流程”这种东西作过说明。OpenGL是按照什么样的流程来进行工做的呢?下面的图片能够简要的说明一下:

声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,因为该书的旧版(初版,1994年)已经流传于网络,我但愿没有触及到版权问题。

由于图片中的文字是英语,这里还翻译一下。说明文字也夹杂在翻译之中了。

1. Vertex data: 顶点数据。好比咱们指定的颜色、纹理坐标、法线向量、顶点坐标等,都属于顶点数据。

2. Pixel data: 像素数据。咱们在绘制像素、指定纹理时都会用到像素数据。

3. Display list: 显示列表。能够把调用的OpenGL函数保存起来。(参见第八课)

4. Evaluators: 求值器。这个咱们在前面的课程中没有提到,之后估计也不太会提到。利用求值器能够指定贝赛尔曲线或者贝赛尔曲面,可是实际上仍是能够理解为指定顶点、指定纹理坐标、指定法线向量等。

5. Per-vertex operations and primitive assembly: 单一的顶点操做以及图元装配。首先对单一的顶点进行操做,好比变换(参见第五课)。而后把顶点装配为图元(图元就是OpenGL所能绘制的最简单的图形,好比点、线段、三角形、四边形、多边形等,参见第二课)

6. Pixel operations: 像素操做。例如把内存中的像素数据格式转化为图形硬件所支持的数据格式。对于纹理,能够替换其中的一部分像素,这也属于像素操做。

7. Rasterization: 光栅化。顶点数据和像素数据在这里交汇(能够想像成:顶点和纹理,一块儿组合成了具备纹理的三角形),造成完整的、能够显示的一整块(多是点、线段、三角形、四边形,或者其它不规则图形),里面包含若干个像素。这一整块被称为fragment(片断)。

8. Per-fragment operations: 片断操做。包括各类片断测试(参见第十二课)。

9. Framebuffer: 帧缓冲。这是一块存储空间,显示设备从这里读取数据,而后显示到屏幕。

10. Texture assembly: 纹理装配,这里我也没怎么弄清楚:(,大概是说纹理的操做和像素操做是相关的吧。

说明:图片中实线表示正常的处理流程,虚线表示数据能够反方向读取,好比能够用glReadPixels从帧缓冲中读取像素数据(其实是从帧缓冲读取数据,通过像素操做,把显示设备中的像素数据格式转化为内存中的像素数据格式,最终成为内存中的像素数据)。

小结

本课是枯燥的理论知识。

OpenGL是一个状态机,它维持本身的状态,并根据用户调用的函数来改变本身的状态。根据状态的不一样,调用一样的函数也可能产生不一样的效果。

能够经过一些函数来获取OpenGL当前的状态。经常使用的函数有:glIsEnabled, glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev。

OpenGL的工做流程,输入像素数据和顶点数据,两种数据分别操做后,经过光栅化,获得片断,再通过片断处理,最后绘制到帧缓冲区。绘制的结果也能够逆方向传送,最终转化为像素数据。



OpenGL入门学习[十四]


OpenGL从推出到如今,已经有至关长的一段时间了。其间,OpenGL不断的获得更新。到今天为止,正式的OpenGL已经有九个版本。(1.0, 1.1, 1.2, 1.2.1, 1.3, 1.4, 1.5, 2.0, 2.1)
每一个OpenGL版本的推出,都增长了一些当时流行的或者迫切须要的新功能。同时,到如今为止,OpenGL是向下兼容的,就是说若是某个功能在一个低版本中存在,则在更高版本中也必定存在。这一特性也为咱们编程提供了一点方便。
当前OpenGL的最新版本是OpenGL 2.1,可是并非全部的计算机系统都有这样最新版本的OpenGL实现。举例来讲,Windows系统若是没有安装显卡驱动,或者显卡驱动中没有附带OpenGL,则Windows系统默认提供一个软件实现的OpenGL,它没有使用硬件加速,所以速度可能较慢,版本也很低,仅支持1.1版本(据说Windows Vista默认提供的OpenGL支持到1.4版本,我也不太清楚)。nVidia和ATI这样的显卡巨头,其主流显卡基本上都提供了对OpenGL 2.1的支持。但一些旧型号的显卡由于性能不足等缘由,只能支持到OpenGL 2.0或者OpenGL 1.5。Intel的集成显卡,不少都只提供了OpenGL 1.4(听说目前也有更高版本的了,可是我没有见到)。
OpenGL 2.0是一次比较大的改动,也所以升级了主版本号。能够认为OpenGL 2.0版本是一个分水岭,是否支持OpenGL 2.0版本,直接关系到运行OpenGL程序时的效果。若是要类比一下的话,我以为OpenGL 1.5和OpenGL 2.0的差距,就像是DirectX 8.1和DirectX 9.0c的差距了。
检查本身的OpenGL版本
能够很容易的知道本身系统中的OpenGL版本,方法就是调用glGetString函数。 

const  char* version = ( const  char*)glGetString(GL_VERSION);
printf( "OpenGL 版本:%s\n", version);



glGetString(GL_VERSION);会返回一个表示版本的字符串,字符串的格式为X.X.X,就是三个整数,用小数点隔开,第一个数表示OpenGL主版本号,第二个数表示OpenGL次版本号,第三个数表示厂商发行代号。好比我在运行时获得的是"2.0.1",这表示个人OpenGL版本为2.0(主版本号为2,次版本号为0),是厂商的第一个发行版本。
经过sscanf函数,也能够把字符串分红三个整数,以便详细的进行判断。 

int main_version, sub_version, release_version;
const  char* version = ( const  char*)glGetString(GL_VERSION);
sscanf(version,  "%d.%d.%d", &main_version, &sub_version, &release_version);
printf( "OpenGL 版本:%s\n", version);
printf( "主版本号:%d\n", main_version);
printf( "次版本号:%d\n", sub_version);
printf( "发行版本号:%d\n", release_version);



glGetString还能够取得其它的字符串。
glGetString(GL_VENDOR); 返回OpenGL的提供厂商。
glGetString(GL_RENDERER); 返回执行OpenGL渲染的设备,一般就是显卡的名字。
glGetString(GL_EXTENSIONS); 返回所支持的全部扩展,每两个扩展之间用空格隔开。详细状况参见下面的关于“OpenGL扩展”的叙述。
版本简要历史
版本不一样,提供功能的多少就不一样。这里列出每一个OpenGL版本推出时,所增长的主要功能。固然每一个版本的修改并不仅是下面的内容,读者若是须要知道更详细的情形,能够查阅OpenGL标准。
OpenGL 1.1
顶点数组。把全部的顶点数据(颜色、纹理坐标、顶点坐标等)都放到数组中,能够大大的减小诸如glColor*, glVertex*等函数的调用次数。虽然显示列表也能够减小这些函数的调用次数,可是显示列表中的数据是不能够修改的,顶点数组中的数据则能够修改。
纹理对象。把纹理做为对象来管理,同一时间OpenGL能够保存多个纹理(但只使用其中一个)。之前没有纹理对象时,OpenGL只能保存一个“当前纹理”。要使用其它纹理时,只能抛弃当前的纹理,从新载入。原来的方式很是影响效率。
OpenGL 1.2
三维纹理。之前的OpenGL只支持一维、二维纹理。
像素格式。新增长了GL_BGRA等原来没有的像素格式。容许压缩的像素格式,例如GL_UNSIGNED_SHORT_5_5_5_1格式,表示两个字节,存放RGBA数据,其中R, G, B各占5个二进制位,A占一个二进制位。
图像处理。新增了一个“图像处理子集”,提供一些图像处理的专用功能,例如卷积、计算柱状图等。这个子集虽然是标准规定,可是OpenGL实现时也能够选择不支持它。
OpenGL 1.2.1
没有加入任何新的功能。可是引入了“ARB扩展”的概念。详细状况参见下面的关于“OpenGL扩展”的叙述。
OpenGL 1.3
压缩纹理。在处理纹理时,使用压缩后的纹理而不是纹理自己,这样能够节省空间(节省显存)和传输带宽(节省从内存到显存的数据流量)
多重纹理。同时使用多个纹理。
多重采样。一种全屏抗锯齿技术,使用后可让画面显示更加平滑,减轻锯齿现象。对于nvidia显卡,在设置时有一项“3D平滑处理设置”,实际上就是多重采样。一般能够选择2x, 4x,高性能的显卡也能够选择8x, 16x。其它显卡也几乎都有相似的设置选项,可是也有的显卡不支持多重采样,因此是0x。
OpenGL 1.4
深度纹理。能够把深度值像像素值同样放到纹理中,在绘制阴影时特别有用。
辅助颜色。顶点除了有颜色外还有辅助颜色。在使用光照时能够表现出更真实的效果。
OpenGL 1.5
缓冲对象。容许把数据(主要指顶点数据)交由OpenGL保存到较高性能的存储器中,提升绘制速度。比顶点数组有更多优点。顶点数组只是减小函数调用次数,缓冲对象不只减小函数调用次数,还加快数据访问速度。
遮挡查询。能够计算一个物体有几个像素会被绘制到屏幕上。若是物体没有任何像素会被绘制,则不须要加载相关的数据(例如纹理数据)。
OpenGL 2.0
可编程着色。容许编写一小段代码来代替OpenGL原来的顶点操做/片断操做。这样提供了巨大的灵活性,能够实现各类各样的丰富的效果。
纹理大小再也不必须是2的整数次方。
点块纹理。把纹理应用到一个点(大小可能不仅一个像素)上,这样比绘制一个矩形可能效率更高。
OpenGL 2.1
可编程着色,编程语言由原来的1.0版本升级为1.2版本。
缓冲对象,原来仅容许存放顶点数据,如今也容许存放像素数据。
得到新版本的OpenGL
要得到新版本OpenGL,首先应该登录你的显卡厂商网站,并查询相关的最新信息。根据状况,下载最新的驱动或者OpenGL软件包。
若是本身的显卡不支持高版本的OpenGL,或者本身的操做系统根本就没有提供OpenGL,怎么办呢?有一个被称为MESA的开源项目,用C语言编写了一个OpenGL实现,最新的mesa 7.0已经实现了OpenGL 2.1标准中所规定的各类功能。下载MESA的代码,而后编译,就能够获得一个最新版本的OpenGL了。呵呵,不要高兴的太早。MESA是软件实现的,就是说没有用到硬件加速,所以运行起来会较慢,尤为是使用新版本的OpenGL所规定的一些高级特性时,慢得几乎没法忍受。MESA不能让你用旧的显卡玩新的游戏(极可能慢得无法玩),可是若是你只是想学习或尝试一下新版本OpenGL的各类功能,MESA能够知足你的一部分要求。
OpenGL扩展
OpenGL版本的更新并不快。若是某种技术变得流行起来,可是OpenGL标准中又没有相关的规定对这种技术提供支持,那就只能经过扩展来实现了。
厂商在发行OpenGL时,除了遵守OpenGL标准,提供标准所规定的各类功能外,每每还提供其它一些额外的功能,这就是扩展。
扩展的存在,使得各类新的技术能够迅速的被应用到OpenGL中。好比“多重纹理”,它是在OpenGL 1.3中才被加入到标准中的,在OpenGL 1.3出现之前,不少OpenGL实现都经过扩展来支持“多重纹理”。这样,即便OpenGL版本不更新,只要增长新的扩展,也能够提供新的功能了。这也说明,即便OpenGL版本较低,也不必定不支持一些高版本OpenGL才提供的功能。实际上某些OpenGL 1.5的实现,也可能提供了最新的OpenGL 2.1版本所规定的大部分功能。
固然扩展也有缺点,那就是程序在运行的时候必须检查每一个扩展功能是否被支持,致使编写程序代码复杂。

扩展的名字
每一个OpenGL扩展,都必须向OpenGL的网站注册,确认后才能成为扩展。注册后的扩展有编号和名字。编号仅仅是一个序号,名字则与扩展所提供的功能相关。
名字用下划线分为三部分。举例来讲,一个扩展的名字可能为:GL_NV_half_float,其意义以下:
第一部分为扩展的目标。好比GL表示这是一个OpenGL扩展。若是是WGL则表示这是一个针对Windows的OpenGL扩展,若是是GLX则表示这是一个针对linux的X Window系统的OpenGL扩展。
第二部分为提供扩展的厂商。好比NV表示这是nVidia公司所提供的扩展。相应的还有ATI, IBM, SGI, APPLE, MESA等。
剩下的部分就表示扩展所提供的内容了。好比half_float,表示半精度的浮点数,每一个浮点数的精度只有单精度浮点数的一半,所以只须要两个字节就能够保存。这种扩展功能能够节省内存空间,也节省从内存到显卡的数据传输量,代价就是精确度有所下降。
EXT扩展和ARB扩展
最初的时候,每一个厂商都提供本身的扩展。这样致使的结果就是,即便是提供相同的功能,不一样的厂商却提供不一样的扩展,这样在编写程序的时候,使用一种功能就须要依次检查每一个可能支持这种功能的扩展,很是繁琐。
因而出现了EXT扩展和ARB扩展。
EXT扩展是由多个厂商共同协商后造成的扩展,在扩展名字中,“提供扩展的厂商”一栏将再也不是具体的厂商名,而是EXT三个字母。好比GL_EXT_bgra,就是一个EXT扩展。
ARB扩展不只是由多个厂商共同协商造成,还须要通过OpenGL体系结构审核委员会(即ARB)的确认。在扩展名字中,“提供扩展的厂商”一栏再也不是具体的厂商名字,而是ARB三个字母。好比GL_ARB_imaging,就是一个ARB扩展。
一般,一种功能若是有多个厂商提出,则它成为EXT扩展。在之后的时间里,若是通过了ARB确认,则它成为ARB扩展。再日后,若是OpenGL的维护者认为这种功能须要加入到标准规定中,则它再也不是扩展,而成为标准的一部分。
例如point_parameters,就是先有GL_EXT_point_parameters,再有GL_ARB_point_parameters,最后到OpenGL 1.4版本时,这个功能为标准规定必须提供的功能,再也不是一个扩展。
在使用OpenGL所提供的功能时,应该按照标准功能、ARB扩展、EXT扩展、其它扩展这样的优先顺序。例若有ARB扩展支持这个功能时,就不使用EXT扩展。
在程序中,判断OpenGL是否支持某个扩展
前面已经说过,glGetString(GL_EXTENSIONS)会返回当前OpenGL所支持的全部扩展的名字,中间用空格分开,这就是咱们判断是否支持某个扩展的依据。 

#include < string.h>
// 判断OpenGL是否支持某个指定的扩展
// 若支持,返回1。不然返回0。
int hasExtension( const  char* name) {
     const  char* extensions = ( const  char*)glGetString(GL_EXTENSIONS);
     const  char* end = extensions +  strlen(extensions);
     size_t name_length =  strlen(name);
     while( extensions < end ) {
         size_t position =  strchr(extensions, ' ') - extensions;
         if( position == name_length &&
                 strncmp(extensions, name, position) == 0 )
             return 1;
         extensions += (position + 1);
     }
     return 0;
}



上面这段代码,判断了OpenGL是否支持指定的扩展,能够看到,判断时彻底是靠字符串处理来实现的。循环检测,找到第一个空格,而后比较空格以前的字符串是否与指定的名字一致。若一致,说明扩展是被支持的;不然,继续比较。若全部内容都比较完,则说明扩展不被支持。
编写程序调用扩展的功能
扩展的函数、常量,在命名时与一般的OpenGL函数、常量有少量区别。那就是扩展的函数、常量将以厂商的名字做为后缀。
好比ARB扩展,全部ARB扩展的函数,函数名都以ARB结尾,常量名都以_ARB结尾。例如:
glGenBufferARB(函数)
GL_ARRAY_BUFFER_ARB(常量)
若是已经知道OpenGL支持某个扩展,则如何调用扩展中的函数?大体的思路就是利用函数指针。可是不幸的是,在不一样的操做系统中,取得这些函数指针的方法各不相同。为了可以在各个操做系统中都能顺利的使用扩展,我向你们介绍一个小巧的工具:GLEE。
GLEE是一个开放源代码的项目,能够从网络上搜索并下载。其代码由两个文件组成,一个是GLee.c,一个是GLee.h。把两个文件都放到本身的源代码一块儿编译,运行的时候,GLee能够自动的判断全部扩展是否被支持,若是支持,GLEE会自动读取对应的函数,供咱们调用。
咱们本身编写代码时,须要首先包含GLee.h,而后才包含GL/glut.h(注意顺序不能调换),而后就能够方便的使用各类扩展功能了。

#include  "GLee.h"
#include <GL/glut.h> // 注意顺序,GLee.h要在glut.h以前使用



GLEE也能够帮助咱们判断OpenGL是否支持某个扩展,所以有了GLEE,前面那个判断是否支持扩展的函数就不太必要了。
示例代码
让咱们用一段示例代码结束本课。
咱们选择一个目前绝大多数显卡都支持的扩展GL_ARB_window_pos,来讲明如何使用GLEE来调用OpenGL扩展功能。一般咱们在绘制像素时,须要用glRasterPos*函数来指定绘制的位置。可是,glRasterPos*函数使用的不是屏幕坐标,例如指定(0, 0)不必定是左下角,这个坐标须要通过各类变换(参见第五课,变换),最后才获得屏幕上的窗口位置。
经过GL_ARB_window_pos扩展,咱们能够直接用屏幕上的坐标来指定绘制的位置,再也不须要通过变换,这样在不少场合会显得简单。 

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

void display( void) {
     glClear(GL_COLOR_BUFFER_BIT);

     if( GLEE_ARB_window_pos ) { // 若是支持GL_ARB_window_pos
                                 // 则使用glWindowPos2iARB函数,指定绘制位置
         printf( "支持GL_ARB_window_pos\n");
         printf( "使用glWindowPos函数\n");
         glWindowPos2iARB(100, 100);
     }  else {                     // 若是不支持GL_ARB_window_pos
                                 // 则只能使用glRasterPos*系列函数
                                 // 先计算出一个通过变换后可以获得
                                 //    (100, 100)的坐标(x, y, z)
                                 // 而后调用glRasterPos3d(x, y, z);
         GLint viewport[4];
         GLdouble modelview[16], projection[16];
         GLdouble x, y, z;

         printf( "不支持GL_ARB_window_pos\n");
         printf( "使用glRasterPos函数\n");

         glGetIntegerv(GL_VIEWPORT, viewport);
         glGetDoublev(GL_MODELVIEW_MATRIX, modelview);
         glGetDoublev(GL_PROJECTION_MATRIX, projection);
         gluUnProject(100, 100, 0.5, modelview, projection, viewport,
             &x, &y, &z);
         glRasterPos3d(x, y, z);
     }

     { // 绘制一个5*5的像素块
         GLubyte pixels[5][5][4];
         // 把像素中的全部像素都设置为红色
         int i, j;
         for(i=0; i<5; ++i)
             for(j=0; j<5; ++j) {
                 pixels[i][j][0] = 255; // red
                 pixels[i][j][1] = 0;    // green
                 pixels[i][j][2] = 0;    // blue
                 pixels[i][j][3] = 255; // alpha
             }
         glDrawPixels(5, 5, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
     }

     glutSwapBuffers();
}

int  main( int argc,  char* argv[]) {
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(512, 512);
     glutCreateWindow( "OpenGL");
     glutDisplayFunc(&display);
     glutMainLoop();
}



能够看到,使用了扩展之后,代码会简单得多了。不支持GL_ARB_window_pos扩展时必须使用较多的代码才能实现的功能,使用GL_ARB_window_pos扩展后便可简单的解决。
若是把代码修改一下,不使用扩展而直接使用else里面的代码,能够发现运行效果是同样的。
工具软件
在课程的最后我还向你们介绍一个免费的工具软件,这就是OpenGL Extension Viewer(各大软件网站均有下载,请本身搜索之),目前较新的版本是3.0。
这个软件能够查看本身计算机系统的OpenGL信息。包括OpenGL版本、提供厂商、设备名称、所支持的扩展等。
软件能够查看的信息很详细,好比查看容许的最大纹理大小、最大光源数目等。
在查看扩展时,能够在最下面一栏输入扩展的名字,按下回车后便可链接到OpenGL官方网站,查找关于这个扩展的详细文档,很是不错。
能够根据电脑的配置状况,自动链接到对应的官方网站,方便下载最新驱动。(好比我是nVidia的显卡,则链接到nVidia的驱动下载页面)
能够进行OpenGL测试,看看运行起来性能如何。
能够给出整体报告,若是一些比较重要的功能不被支持,则会用粗体字标明。
软件还带有一个数据库,能够查询各厂商、各型号的显卡对OpenGL各类扩展的支持状况。
小结

本课介绍了OpenGL版本和OpenGL扩展。
OpenGL从诞生到如今,经历了1.0, 1.1, 1.2, 1.2.1, 1.3, 1.4, 1.5, 2.0, 2.1这些版本。
每一个系统中的OpenGL版本可能不一样。使用glGetString(GL_VERSION);能够查看当前的OpenGL版本。
新版本的OpenGL将兼容旧版本的OpenGL,同时提供更多的新特性和新功能。
OpenGL在实现时能够经过扩展,来提供额外的功能。
OpenGL扩展有厂家扩展、EXT扩展、ARB扩展。一般应该尽可能使用标准功能,其次才是ARB扩展、EXT扩展、厂家扩展。
GLEE是一个能够无偿使用的工具,使用它能够方便的判断当前的OpenGL是否支持某扩展,也能够方便的调用扩展。
OpenGL Extension Viewer是一个软件,能够检查系统所支持OpenGL的版本、支持的扩展、以及不少的详细信息。


OpenGL入门学习[十五]


此次讲的全部内容都装在一个立方体中,呵呵。
呵呵,绘制一个立方体,简单呀,咱们学了第一课第二课,早就会了。
先别着急,立方体是很简单,可是这里只是拿立方体作一个例子,来讲明OpenGL在绘制方法上的改进。
从原始一点的办法开始
一个立方体有六个面,每一个面是一个正方形,好,绘制六个正方形就能够了。

glBegin(GL_QUADS);
     glVertex3f(...);
     glVertex3f(...);
     glVertex3f(...);
     glVertex3f(...);

     // ...
glEnd();



为了绘制六个正方形,咱们为每一个正方形指定四个顶点,最终咱们须要指定6*4=24个顶点。可是咱们知道,一个立方体其实总共只有八个顶点,要指定24次,就意味着每一个顶点其实重复使用了三次,这样可不是好的现象。最起码,像上面这样重复烦琐的代码,是很容易出错的。稍有不慎,即便相同的顶点也可能被指定成不一样的顶点了。
若是咱们定义一个数组,把八个顶点都放到数组里,而后每次指定顶点都使用指针,而不是使用直接的数据,这样就避免了在指定顶点时考虑大量的数据,因而减小了代码出错的可能性。

// 将立方体的八个顶点保存到一个数组里面
static  const GLfloat vertex_list[][3] = {
     -0.5f, -0.5f, -0.5f,
      0.5f, -0.5f, -0.5f,
     // ...
};
// 指定顶点时,用指针,而不用直接用具体的数据
glBegin(GL_QUADS);
     glVertex3fv(vertex_list[0]);
     glVertex3fv(vertex_list[2]);
     glVertex3fv(vertex_list[3]);
     glVertex3fv(vertex_list[1]);

     // ...
glEnd();



修改以后,虽然代码变长了,可是确实易读得多。很容易就看出第0, 2, 3, 1这四个顶点构成一个正方形。
稍稍观察就能够发现,咱们使用了大量的glVertex3fv函数,其实每一句都只有其中的顶点序号不同,所以咱们能够再定义一个序号数组,把全部的序号也放进去。这样一来代码就更加简单了。

// 将立方体的八个顶点保存到一个数组里面
static  const GLfloat vertex_list[][3] = {
     -0.5f, -0.5f, -0.5f,
      0.5f, -0.5f, -0.5f,
     -0.5f,   0.5f, -0.5f,
      0.5f,   0.5f, -0.5f,
     -0.5f, -0.5f,   0.5f,
      0.5f, -0.5f,   0.5f,
     -0.5f,   0.5f,   0.5f,
      0.5f,   0.5f,   0.5f,
};

// 将要使用的顶点的序号保存到一个数组里面
static  const GLint index_list[][4] = {
     0, 2, 3, 1,
     0, 4, 6, 2,
     0, 1, 5, 4,
     4, 5, 7, 6,
     1, 3, 7, 5,
     2, 6, 7, 3,
};

int i, j;

// 绘制的时候代码很简单
glBegin(GL_QUADS);
for(i=0; i<6; ++i)          // 有六个面,循环六次
     for(j=0; j<4; ++j)      // 每一个面有四个顶点,循环四次
         glVertex3fv(vertex_list[index_list[i][j]]);
glEnd();



这样,咱们就获得一个比较成熟的绘制立方体的版本了。它的数据和程序代码基本上是分开的,全部的顶点放到一个数组中,使用顶点的序号放到另外一个数组中,而利用这两个数组来绘制立方体的代码则很简单。
关于顶点的序号,下面这个图片能够帮助理解。
http://blog.programfan.com/upfile/200805/2008050513265.gif

正对咱们的面,按逆时针顺序,背对咱们的面,则按顺时针顺序,这样就获得了上面那个index_list数组。
为何要按照顺时针逆时针的规则呢?由于这样作能够保证不管从哪一个角度观察,看到的都是“正面”,而不是背面。在计算光照时,正面和背面的处理多是不一样的,另外,剔除背面只绘制正面,能够提升程序的运行效率。(关于正面、背面,以及剔除,参见第三课,绘制几何图形的一些细节问题)
例如在绘制以前调用以下的代码:

glFrontFace(GL_CCW);
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);


则绘制出来的图形就只有正面,而且只显示边线,不进行填充。
效果如图:
http://blog.programfan.com/upfile/200805/20080505132612.gif
顶点数组
(提示:顶点数组是OpenGL 1.1所提供的功能)
前面的方法中,咱们将数据和代码分离开,看起来只要八个顶点就能够绘制一个立方体了。可是实际上,循环仍是执行了6*4=24次,也就是说虽然代码的结构清晰了很多,可是程序运行的效率,仍是和最原始的那个方法同样。
减小函数的调用次数,是提升运行效率的方法之一。因而咱们想到了显示列表。把绘制立方体的代码装到一个显示列表中,之后只要调用这个显示列表便可。
这样看起来很不错,可是显示列表有一个缺点,那就是一旦创建后不可再改。若是咱们要绘制的不是立方体,而是一个可以走动的人物,由于人物走动时,四肢的位置不断变化,几乎没有办法把全部的内容装到一个显示列表中。必须每种动做都使用单独的显示列表,这样会致使大量的显示列表管理困难。
顶点数组是解决这个问题的一个方法。使用顶点数组的时候,也是像前面的方法同样,用一个数组保存全部的顶点,用一个数组保存顶点的序号。但最后绘制的时候,不是编写循环语句逐个的指定顶点了,而是通知OpenGL,“保存顶点的数组”和“保存顶点序号的数组”所在的位置,由OpenGL自动的找到顶点,并进行绘制。
下面的代码说明了顶点数组是如何使用的:

glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertex_list);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);



其中:
glEnableClientState(GL_VERTEX_ARRAY); 表示启用顶点数组。
glVertexPointer(3, GL_FLOAT, 0, vertex_list); 指定顶点数组的位置,3表示每一个顶点由三个量构成(x, y, z),GL_FLOAT表示每一个量都是一个GLfloat类型的值。第三个参数0,参见后面介绍“stride参数”。最后的vertex_list指明了数组实际的位置。
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list); 根据序号数组中的序号,查找到相应的顶点,并完成绘制。GL_QUADS表示绘制的是四边形,24表示总共有24个顶点,GL_UNSIGNED_INT表示序号数组内每一个序号都是一个GLuint类型的值,index_list指明了序号数组实际的位置。
上面三行代码代替了原来的循环。能够看到,原来的glBegin/glEnd再也不须要了,也不须要调用glVertex*系列函数来指定顶点,所以能够明显的减小函数调用次数。另外,数组中的内容能够随时修改,比显示列表更加灵活。 

详细一点的说明。
顶点数组其实是多个数组,顶点坐标、纹理坐标、法线向量、顶点颜色等等,顶点的每个属性均可以指定一个数组,而后用统一的序号来进行访问。好比序号3,就表示取得颜色数组的第3个元素做为颜色、取得纹理坐标数组的第3个元素做为纹理坐标、取得法线向量数组的第3个元素做为法线向量、取得顶点坐标数组的第3个元素做为顶点坐标。把全部的数据综合起来,最终获得一个顶点。
能够用glEnableClientState/glDisableClientState单独的开启和关闭每一种数组。
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
用如下的函数来指定数组的位置:
glVertexPointer
glColorPointer
glNormalPointer
glTexCoordPointer 

为何不使用原来的glEnable/glDisable函数,而要专门的规定一个glEnableClientState/glDisableClientState函数呢?这跟OpenGL的工做机制有关。OpenGL在设计时,认为能够将整个OpenGL系统分为两部分,一部分是客户端,它负责发送OpenGL命令。一部分是服务端,它负责接收OpenGL命令并执行相应的操做。对于我的计算机来讲,能够将CPU、内存等硬件,以及用户编写的OpenGL程序看作客户端,而将OpenGL驱动程序、显示设备等看作服务端。
一般,全部的状态都是保存在服务端的,便于OpenGL使用。例如,是否启用了纹理,服务端在绘制时常常须要知道这个状态,而咱们编写的客户端OpenGL程序只在不多的时候须要知道这个状态。因此将这个状态放在服务端是比较有利的。
但顶点数组的状态则不一样。咱们指定顶点,实际上就是把顶点数据从客户端发送到服务端。是否启用顶点数组,只是控制发送顶点数据的方式而已。服务端只管接收顶点数据,而没必要管顶点数据究竟是用哪一种方式指定的(能够直接使用glBegin/glEnd/glVertex*,也能够使用顶点数组)。因此,服务端不须要知道顶点数组是否开启。所以,顶点数组的状态放在客户端是比较合理的。
为了表示服务端状态和客户端状态的区别,服务端的状态用glEnable/glDisable,客户端的状态则用glEnableClientState/glDisableClientState。
stride参数。
顶点数组并不要求全部的数据都连续存放。若是数据没有连续存放,则指定数据之间的间隔便可。
例如:咱们使用一个struct来存放顶点中的数据。注意每一个顶点除了坐标外,还有额外的数据(这里是一个int类型的值)。

typedef  struct __point__ {
     GLfloat position[3];
     int      id;
} Point;
Point vertex_list[] = {
     -0.5f, -0.5f, -0.5f, 1,
      0.5f, -0.5f, -0.5f, 2,
     -0.5f,   0.5f, -0.5f, 3,
      0.5f,   0.5f, -0.5f, 4,
     -0.5f, -0.5f,   0.5f, 5,
      0.5f, -0.5f,   0.5f, 6,
     -0.5f,   0.5f,   0.5f, 7,
      0.5f,   0.5f,   0.5f, 8,
};
static GLint index_list[][4] = {
     0, 2, 3, 1,
     0, 4, 6, 2,
     0, 1, 5, 4,
     4, 5, 7, 6,
     1, 3, 7, 5,
     2, 6, 7, 3,
};
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT,  sizeof(Point), vertex_list);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);



注意最后三行代码,能够看到,几乎全部的地方都和原来同样,只在glVertexPointer函数的第三个参数有所不一样。这个参数就是stride,它表示“从一个数据的开始到下一个数据的开始,所相隔的字节数”。这里设置为sizeof(Point)就刚恰好。若是设置为0,则表示数据是紧密排列的,对于3个GLfloat的状况,数据紧密排列时stride实际上为3*4=12。
混合数组。若是须要同时使用颜色数组、顶点坐标数组、纹理坐标数组、等等,有一种方式是把全部的数据都混合起来,指定到同一个数组中。这就是混合数组。

GLfloat arr_c3f_v3f[] = {
     1, 0, 0, 0, 1, 0,
     0, 1, 0, 1, 0, 0,
     0, 0, 1, -1, 0, 0,
};
GLuint index_list[] = {0, 1, 2};
glInterleavedArrays(GL_C3F_V3F, 0, arr_c3f_v3f);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, index_list);



glInterleavedArrays,能够设置混合数组。这个函数会自动调用glVertexPointer, glColorPointer等函数,而且自动的开启或禁用相关的数组。
函数的第一个参数表示了混合数组的类型。例如GL_C3F_V3F表示:三个浮点数做为颜色、三个浮点数做为顶点坐标。也能够有其它的格式,好比GL_V2F, GL_V3F, GL_C4UB_V2F, GL_C4UB_V3F, GL_C3F_V3F, GL_N3F_V3F, GL_C4F_N3F_V3F, GL_T2F_V3F, GL_T4F_V4F, GL_T2F_C4UB_V3F, GL_T2F_C3F_V3F, GL_T2F_N3F_V3F, GL_T2F_C4F_N3F_V3F, GL_T4F_C4F_N3F_V4F等等。其中T表示纹理坐标,C表示颜色,N表示法线向量,V表示顶点坐标。
再来讲说顶点数组与显示列表的区别。二者均可以明显的减小函数的调用次数,可是仍是各有优势的。
对于顶点数组,顶点数据是存放在内存中的,也就是存放在客户端。每次绘制的时候,须要把全部的顶点数据从客户端(内存)发送到服务端(显示设备),而后进行处理。对于显示列表,顶点数据是放在显示列表中的,显示列表自己又是存放在服务器端的,因此不会重复的发送数据。
对于顶点数组,由于顶点数据放在内存中,因此能够随时修改,每次绘制的时候都会把当前数组中的内容做为顶点数据发送并进行绘制。对于显示列表,数据已经存放到服务器段,而且没法取出,因此没法修改。
也就是说,显示列表能够避免数据的重复发送,效率会较高;顶点数组虽然会重复的发送数据,但因为数据能够随时修改,灵活性较好
顶点缓冲区对象
(提示:顶点缓冲区对象是OpenGL 1.5所提供的功能,但它在成为标准前是一个ARB扩展,能够经过GL_ARB_vertex_buffer_object扩展来使用这项功能。前面已经讲过,ARB扩展的函数名称以字母ARB结尾,常量名称以字母_ARB结尾,而标准函数、常量则去掉了ARB字样。不少的OpenGL实现同时支持vertex buffer object的标准版本和ARB扩展版本。咱们这里以ARB扩展来说述,由于目前绝大多数我的计算机都支持ARB扩展版本,但少数显卡仅支持OpenGL 1.4,没法使用标准版本。)
前面说到顶点数组和显示列表在绘制立方体时各有优劣,那么有没有办法将它们的优势集中到一块儿,而且尽量的减小缺点呢?顶点缓冲区对象就是为了解决这个问题而诞生的。它数据存放在服务端,同时也容许客户端灵活的修改,兼顾了运行效率和灵活性。
顶点缓冲区对象跟纹理对象有不少类似之处。首先,分配一个缓冲区对象编号,而后,为对应编号的缓冲区对象指定数据,之后能够随时修改其中的数据。下面的表格能够帮助类比理解。 

                                   纹理对象          顶点缓冲区对象
分配编号                           glGenTextures     glGenBuffersARB
绑定(指定为当前所使用的对象)     glBindTexture     glBindBufferARB
指定数据                           glTexImage*       glBufferDataARB
修改数据                           glTexSubImage*    glBufferSubDataARB



顶点数据和序号各自使用不一样的缓冲区。具体的说,就是顶点数据放在GL_ARRAY_BUFFER_ARB类型的缓冲区中,序号数据放在GL_ELEMENT_ARRAY_BUFFER_ARB类型的缓冲区中。
具体的状况能够用下面的代码来讲明:

static GLuint vertex_buffer;
static GLuint index_buffer;

// 分配一个缓冲区,并将顶点数据指定到其中
glGenBuffersARB(1, &vertex_buffer);
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);
glBufferDataARB(GL_ARRAY_BUFFER_ARB,
     sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB);

// 分配一个缓冲区,并将序号数据指定到其中
glGenBuffersARB(1, &index_buffer);
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);
glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB,
     sizeof(index_list), index_list, GL_STATIC_DRAW_ARB);



在指定缓冲区数据时,最后一个参数是关于性能的提示。一共有STREAM_DRAW, STREAM_READ, STREAM_COPY, STATIC_DRAW, STATIC_READ, STATIC_COPY, DYNAMIC_DRAW, DYNAMIC_READ, DYNAMIC_COPY这九种。每一种都表示了使用频率和用途,OpenGL会根据这些提示进行必定程度的性能优化。
(提示仅仅是提示,不是硬性规定。也就是说,即便使用了STREAM_DRAW,告诉OpenGL这段缓冲区数据一旦指定,之后不会修改,但实际上之后仍可修改,不过修改时可能有较大的性能代价) 

当使用glBindBufferARB后,各类使用指针为参数的OpenGL函数,行为会发生变化。
以glColor3fv为例,一般,这个函数接受一个指针做为参数,从指针所指的位置取出连续的三个浮点数,做为当前的颜色。
但使用glBindBufferARB后,这个函数再也不从指针所指的位置取数据。函数会先把指针转化为整数,假设转化后结果为k,则会从当前缓冲区的第k个字节开始取数据。特别一点,若是咱们写glColor3fv(NULL);由于NULL转化为整数后一般是零,因此从缓冲区的第0个字节开始取数据,也就是从缓冲区最开始的位置取数据。
这样一来,原来写的

glVertexPointer(3, GL_FLOAT, 0, vertex_list);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);


在使用缓冲区对象后,就变成了

glVertexPointer(3, GL_FLOAT, 0, NULL);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL);

如下是完整的使用了顶点缓冲区对象的代码:

static GLfloat vertex_list[][3] = {
     -0.5f, -0.5f, -0.5f,
      0.5f, -0.5f, -0.5f,
     -0.5f,   0.5f, -0.5f,
      0.5f,   0.5f, -0.5f,
     -0.5f, -0.5f,   0.5f,
      0.5f, -0.5f,   0.5f,
     -0.5f,   0.5f,   0.5f,
      0.5f,   0.5f,   0.5f,
};

static GLint index_list[][4] = {
     0, 2, 3, 1,
     0, 4, 6, 2,
     0, 1, 5, 4,
     4, 5, 7, 6,
     1, 3, 7, 5,
     2, 6, 7, 3,
};

if( GLEE_ARB_vertex_buffer_object ) {
     // 若是支持顶点缓冲区对象
     static  int isFirstCall = 1;
     static GLuint vertex_buffer;
     static GLuint index_buffer;
     if( isFirstCall ) {
         // 第一次调用时,初始化缓冲区
         isFirstCall = 0;

         // 分配一个缓冲区,并将顶点数据指定到其中
         glGenBuffersARB(1, &vertex_buffer);
         glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);
         glBufferDataARB(GL_ARRAY_BUFFER_ARB,
             sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB);

         // 分配一个缓冲区,并将序号数据指定到其中
         glGenBuffersARB(1, &index_buffer);
         glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);
         glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB,
             sizeof(index_list), index_list, GL_STATIC_DRAW_ARB);
     }
     glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);
     glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);

     // 实际使用时与顶点数组很是类似,只是在指定数组时再也不指定实际的数组,改成指定NULL便可
     glEnableClientState(GL_VERTEX_ARRAY);
     glVertexPointer(3, GL_FLOAT, 0, NULL);
     glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL);
else {
     // 不支持顶点缓冲区对象
     // 使用顶点数组
     glEnableClientState(GL_VERTEX_ARRAY);
     glVertexPointer(3, GL_FLOAT, 0, vertex_list);
     glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);
}

能够分配多个缓冲区对象,顶点坐标、颜色、纹理坐标等数据,能够各自单独使用一个缓冲区。
每一个缓冲区能够有不一样的性能提示,好比在绘制一个运动的人物时,顶点坐标数据常常变化,但法线向量、纹理坐标等则不会变化,能够给予不一样的性能提示,以提升性能。
小结

本课从绘制一个立方体出发,描述了OpenGL在各个版本中对于绘制的处理。
绘制物体的时候,应该将数据单独存放,尽可能不要处处写相似glVertex3f(1.0f, 0.0f, 1.0f)这样的代码。将顶点坐标、顶点序号都存放到单独的数组中,可让绘制的代码变得简单。
能够把绘制物体的全部命令装到一个显示列表中,这样能够避免重复的数据传送。可是由于显示列表一旦创建,就没法修改,因此灵活性不好。
OpenGL 1.1版本,提供了顶点数组。它能够指定数据的位置、顶点序号的位置,从而有效的减小函数调用次数,达到提升效率的目的。可是它没有避免重复的数据传送,因此效率还有待进一步提升。
OpenGL 1.5版本,提供了顶点缓冲区对象。它综合了显示列表和顶点数组的优势,同时兼顾运行效率和灵活性,是绘制物体的一个好选择。若是系统不支持OpenGL 1.5,也能够检查是否支持扩展GL_ARB_vertex_buffer_object。



第十六课,在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!='\0'; ++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();
}



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

指定字体
在产生显示列表前,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表示默认字符集)、字体名称。
效果如图:
http://blog.programfan.com/upfile/200805/20080505132624.gif


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

// 计算字符的个数
// 若是是双字节字符的(好比中文字符),两个字节才算一个字符
// 不然一个字节算一个字符
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);



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

void drawCNString( const  char* str) {
     int len, i;
     wchar_twstring;
    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);
}



注意我用了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();
}


效果如图:
http://blog.programfan.com/upfile/200805/20080505132632.gif
纹理字体
把文字放到纹理中有不少好处,例如,能够任意修改字符的大小(而没必要从新指定字体)。
对一面飘动的旗帜使用带有文字的纹理,则文字也会随着飘动。这个技术在“三国志”系列游戏中常常用到,好比关羽的部队,旗帜上就飘着个“关”字,张飞的部队,旗帜上就飘着个“张”字,曹操的大营,旗帜上就飘着个“曹”字。三国人物何其多,不可能为每种姓氏都单独制做一面旗帜纹理,若是可以把文字放到纹理上,则能够解决这个问题。(参见后面的例子:绘制一面“曹”字旗)
如何把文字放到纹理中呢?天然的想法就是:“若是前面所用的显示列表,能够直接往纹理里面绘制,那就行了”。不过,“绘制到纹理”这种技术要涉及的内容可很多,足够咱们专门拿一课的篇幅来说解了。这里咱们不是直接绘制到纹理,而是用简单一点的办法:先把汉字绘制出来,成为像素,而后用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尤为明显。见下图:
http://blog.programfan.com/upfile/200805/20080505132638.gif

因此,须要把绘制的位置往上移一点,具体来讲就是移动下面一栏的高度。这个高度是多少像素呢?这个我也不知道有什么好办法来计算,根据个人经验,移动整个字符高度的八分之一是比较合适的。例如字符大小为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。


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

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

为了完成上面的效果,咱们须要具有如下的知识:
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();
}



http://blog.programfan.com/upfile/200805/20080505132715.gif

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

显示大段的文字


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

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

呃,小小的感慨一下,为何每当我作好一份代码,就发现它实在太长,长到我不想贴出来呢?唉……
先看看图:
http://blog.programfan.com/upfile/200805/20080505132721.gif

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

const  char* g_string =
     "《合金装备》(Metal Gear Solid)结尾曲歌词\n"
    // 歌词不少很长
     "由于。。。。。。。。 \n"
     "美好即将到来\n";

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常识外,更多的会涉及到数学、数据结构与算法、操做系统等各个领域。一个大型的程序一般都要实现一些文字特殊效果,仅仅是调用几个显示列表固然是不行的,须要大量的相关知识来支撑。本课的门槛忽然提升,搞得我都不知道这还算不算是“入门教程”了,但愿各位不要退缩哦。祝你们愉快。 

相关文章
相关标签/搜索