本人同意他人对我的文章引用,但请在引用时注明出处,谢谢.作者:蒋志强
本文章相应的PDF文档和程序及其源代码可以在http://download.csdn.net/source/240256下载
OpenGL的
光照详解
计算机图形学及OpenGL简介
计算机图形学是计算机科学的重要组成部分,在模拟仿真、虚拟现实、飞行员驾驶员训练、医疗、教学、演示等各个方面都得到了广泛得应用。其中最火热的应用是在3D游戏方面,并极大的推动了相关计
算机硬件的高速发展。
我第一次接触
3D
游戏是在小学
6
年纪的时候,当时玩的就是每个游戏爱好者都如雷贯耳的
DOOM
。从那个时候开始,由于游戏商业利润的吸引,相应的计算机硬件的发展速度惊人的迅速,竞争的激烈也可以用残酷来形容。以至于
3D
加速卡曾经的业界老大
3dfx
都走了被
nvida
兼并的一天。
DOS
版本下的
DOOM
正是因为硬件的飞速发展才为计算机图形学在各个领域的广泛应用铺平了道路,让相应的
API
软件开发包有了在现实舞台上一展身手的机会。微软的
3D API
开发包从最早
MS-DOS
下的
DirectX 1.0
到如今
Vista
的
.NET
平台下的
DirectX 10
,
OpenGL
在工业界的事实上的标准的确立,移动平台上的
JAVA 3D
的发展,这些
3D
开发
API
的发展为
3D
开发程序员提供了强大的工具。
在这些
3D API
中,
OpenGL
有着特殊的地位,在工业上被广泛的使用,是事实上的工业标准。
OpenGL
是一个到图形应将爱你的软件接口(
API
),包括
250
个函数,程序员使用它们来创建和控制
3D
交互程序。
OpenGL
是一个独立于硬件的高效接口,可在很多硬件平台上实现,在
UNIX
、
Linux
、
Mactosh
上都可以使用
OpenGL
开发。当然在
PC
上也提供相应的支持,在
PC
游戏史上上有着划时代意义的电子游戏
QUAKE
的
3D
图像在底层就是使用的
OpenGL
。在
OpenGL
之上建立了提供高级特性的复杂函数库。
OpenGL
使用库
(GLU)
提供学多建模特性,包括二次曲面、
NURBS
曲线和曲面。
大多数
OpenGL
都使用相似的操作步骤,这些处理步骤叫做
OpenGL
渲染流水线。如下图所示,虽然并非所有的
OpenGL
实现都使用这种操作顺序,但它为预测
OpenGL
将做什么提供了可靠的线索。
在现实生活中
的物体,要有光照存在才可以被看到。物体通过自身发光以及反射光进入人眼,物体才能在人眼中成像。如果没有任何的光,人眼将观察不到任何东西,一片漆黑。
在光照中首先是光源,要有光源才能产生光线,才有以后的一系列反射、折射、散射等效果。不同的物体的表面物理属性不同,所以相同的光线照射到不同表面属性的物体表面会产生不同的效果,发生漫反射,镜面反射的比例各不相同,有的属于半透明的物体还有折射效果。这些不同的物体表面物理属性属于材质的范畴。
除了材质以外,物体表面还有各种图案效果,这就是纹理。光线在空中穿行的时候,还会有更多复杂的效果,比如灰尘会让光线散射,在空中行成一条光路,直射的阳光产生的光晕效果,炎热的地面温度会让地面附近的光线的行进路线不在是笔直的,从而产生扭曲的效果。
在我的课程报告中,不涉及复杂的光照效果,只对最基本的情况进行介绍,并说明程序实现方法。
在现实中,光源的类型很多,而且有的光源不能简单的用一种模型来描述,而是具有多种不同类型的光源的特点。我们在3D图形程序中,把现实中的光源分为几种典型的种类,基本上可以描述现实中的情况,并进行近似的模拟。这几种基本的光源类型是:点光源、无穷远光源、方向光源和环境光。
点光源:光线从光源点向四面八方发散,发光的恒星(如太阳)、发光的灯泡一般使用该光源模型模拟,是最简单的光源。
无穷远光源:所有的光线都平行的从一个方向过来,当发光体(如太阳)离渲染的场景很远可以认为是无穷远时,一般使用该光源模型进行模拟。
方向光源:光线沿着一个方向在特定角度范围内逐渐发散开。现实世界中的车灯,手电筒一般使用该光源模型进行模拟。
环境光源:光线从各个地方以各个角度投射到场景中所有物体表面,找不到光源的确切位置。现实世界中不存在这样的光源,一般使用该光源模型来模拟点光源、无穷远光源、方向光源在物体表面经过许多次反射后的情况,环境光源照亮所有物体的所有面。
上面这四种基本的光源模型,只能近似的描述光源,不可能做到非常逼真(以假乱真)。因为在现实中的光源,并不真正就是这样的。比如环境光源,一束光线照射到物体表面发生反射后,再照射到另外的物体的表面,如此循环反复这才是环境光的真正情况。但由于这个过程是个无限次反射的过程,计算机无法处理无限的问题,所以采取了简单的近似处理。而且环境光源在反射过程中,上一次反射所带的颜色会影响下次反射所照物体的颜色,并且无限的重复。光线追踪算法是一种好得多的近似描述,但也仅仅是近似描述,只是近似效果比用环境光源模型要好。
除了以上介绍的四种光源模型外,OpenGL还提供了让物体自发光让自己可以被看见的方式。这就是物体自发光。物体自发光对于光源十分的重要,比如电灯泡可以看作是一个点光源,我们把点光源的位置设置到灯泡的中央,这样灯泡周围的物体将被照亮,但是灯泡的外表面由于相对光源来说是背面,将不能被照亮。这与实际情况不符合,灯泡照亮其它物体,而自身却不亮,所以需要通过物体自发光让灯泡的外表面也发亮。
光源的一般属性包括:镜面反射光线颜色、漫反射光颜色、环境光线颜色、光源位置。镜面反射光颜色:在物体表面将发生镜面反射的光线的颜色。漫反射光颜色:在物体表面将发生漫反射的光线的颜色。环境光线颜色:照亮所有物体所有表面的光线的颜色。光源位置就是光源在场景中所在的位置。
光线的衰减:光源发出的光线的强度会随着传播距离越来越大而变弱(无穷远光源除外)。光线强度会乘以一个衰减因子。
衰减因子 = 1/(K1 + K2 * d + k3 *d^2) 其中d为光源距离
(无穷远光源的衰减因子为1)
方向光源发出的光线会随着偏移中心方向的角度增大而减弱。
材质是光照效果中的重要属性。材质描述了物体表面的光学物理属性,决定了光线在该表面光线反射的具体情况。材质决定了物体的表面特性,决定了光线在物体表面反射的情况。
物体表面的反射分为漫反射和镜面反射。
物体反射各种类型光源的情况都可以分为:漫反射和镜面反射两种。
漫反射:光线射到物体表面以后,反射光线的方向是任意方向的。
漫反射
镜面反射:光线射到物体表面以后,反射光线根据照射表面位置的法线方向,发生方向唯一确定的镜面反射。
镜面反射
在OpenGL中漫反射部分的光线与镜面反射部分的光线是分开计算的,然后将分开计算的效果进行叠加。
材质的属性包括:镜面反射颜色、漫反射颜色、环境光颜色、光洁度、自发光颜色。
镜面反射颜色、漫反射色、环境光颜色:分别与光源的镜面反射光颜色、光源的漫反射颜色、光源的环境光颜色共同决定物体表面的镜面反射颜色、漫反射颜色、环境光颜色。3种类型的结果分别计算,然后叠加共同确定反射表面像素值的颜色。
漫反射项: max{L*n,0}×DIFFUSE_light×DIFFUSE_material
环境光项:AMBIENT_light × AMBIENT_material
在OpenGL中可以使用下面的代码来设置材质属性:
GLfloat planet_ambient[] = { 0.01 , 0.01 , 0.01 , 1.0 };
GLfloat planet_diffuse[] = { 0.7 , 0.7 , 0.7 , 1.0 };
glMaterialfv(GL_FRONT , GL_AMBIENT ,planet_ambient);
glMaterialfv(GL_FRONT , GL_DIFFUSE ,planet_diffuse);
OpenGL
确定某个顶点颜色可以分为两种情况:开启光照渲染和关闭光照渲染的情况。当关闭光照渲染的时候,顶点的颜色由OpenGL状态机绘制该物体时的颜色确定;当开启光照渲染的时候,绘制该物体时OpenGL状态机的颜色将对该物体上顶点的颜色没有任何影响,此时物体顶点的颜色由物体材质,光照叠加效果以及物体表面的纹理贴图(如果有纹理贴图的话)共同决定。
在默认情况下OpenGL状态机的光照渲染是关闭的。下面的代码开启光照渲染处理,设置光源的各种属性并将该光源开启。
//
开启光照渲染
glEnable(GL_LIGHTING);
//
设置光源属性
GLfloat light_ambient[] = { 1.0 , 1.0 , 1.0 , 0.0 };
GLfloat light_diffuse[] = { 1.0 , 1.0 , 1.0 , 1.0 };
//
指定光源的位置
GLfloat light _position[] = { 0.0 , 0.0 , 1.0 , 1.0 };
//
用定义好的光源属性给指定光源GL_LIGHT0进行设置
glLightfv(GL_LIGHT0 , GL_AMBIENT , light_ambient);
glLightfv(GL_LIGHT0 , GL_DIFFUSE , light_diffuse);
glLightfv(GL_LIGHT0 , GL_POSITION , light_position);
//
开启设置的光源GL_LIGHT0
glEnable(GL_LIGHT0);
在设置光源的位置的时候需要注意Z轴的正方向是垂直于屏幕向外,当设置Z的值为负值的时候,光源的位置位于屏幕内部;设置Z为正值的时候,光源的位置位于屏幕的外部。如上面的代码设置的位置是屏幕正中央,垂直于屏幕表面外面1个单位的位置。
我们如果要打开全局光照,也就是在没有指定光源的时候,让场景中的物体也可以被照亮,可以使用下面的代码。
//
全局光照系数
GLfloat globel_ambient[] = { 0.0 , 0.0 , 0.0 , 1.0 };
//
打开全局光照
glLightModelfv(GL_LIGHT_MODEL_AMBIENT , globel_ambient);
由于物体有的表面相对于观察点来说,是背向观察者的,即使光源照到了这些表面上的顶点也不能被观察者看到。但是这些顶点也在渲染场景的时候,消耗了大量的计算时间,即使对最后的观察的效果没有任何影响。所以可以设置背面的这些顶点不进行计算,提高程序的运行效率。添加如下的代码:
//
关闭背面顶点的计算
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE , GL_FALSE);
只有环境光和漫反射情况下的程序运行效果如下图:
镜面反射项:max{s*n,0}^shiness×SPECULAR_light×SPECULAR_material
其中s为所计算点的到光源连线的单位向量与所计算点到视点的单位向量的和;shiness是材质的光洁度属性
下面的代码设置了镜面反射:
GLfloat planet_specular[] = { 0.5 , 0.5 , 0.5 , 0.5 };
glMaterialfv(GL_FRONT , GL_SPECULAR ,planet_specular);
加上镜面反射效果的情况后程序运行效果如下图:
一个3D渲染场景中允许有多个光源,3D场景中物体表面的点的最终颜色由各个光源对该点的效果叠加。
一个光源对场景中3D物体表面一个顶点的颜色的贡献如下:
顶点颜色 = 环境光颜色 + 衰减因子 × (漫反射项 + 镜面反射项)
我们将对光源的颜色改为红色,代码如下:
GLfloat planet_diffuse[] = { 0.7 , 0.0, 0.0, 1.0 };
GLfloat planet_specular[] = { 0.5 , 0.0 , 0.0 , 0.5 };
对于场景中的单个光源,修改光源颜色后,程序运行效果如下图:
考虑材质自发光颜色属性和多个光源的后,3D物体表面点的颜色的最终由以下式子决定:
顶点颜色 = 材质自发光颜色项 +
∑(环境光颜色+衰减因子×(漫反射项 + 镜面反射项))
在真实情况下,往往是有众多的光源。比如天上的月亮,路边的街灯,汽车的车灯,商店的霓虹灯等等。OpenGL支持场景中添加多个光源,支持在场景中至少使用8个光源(或者8个以上,者取决于OpenGL的具体实现)。由于OpenGL需要计算机每个顶点从各个光源收到的光线,因此增加光源将增加运算量,降低程序运行的效率,但是增加渲染场景的真实性。
现在在场景中再加入一个绿色的光源,光照的效果将由多个光源的光照效果进行叠加。两个光源加入场景后,程序运行的效果如下图所示:
当场景中的3D物体表面有纹理贴图时,纹理图像也将对点的最终颜色产生影响。
我们在前面已经提到,在这种情况下,物体表面点的颜色由场景中的所有光源(包括环境光源),物体的材质,物体表面的纹理共同确定。
渲染过程是先进行光照处理,再进行纹理处理,纹理处理会影响到最终3D物体表面像素点的颜色。
添加下面的代码,就可以加上纹理贴图:
//
指定纹理图像文件的目录位置
resource_path = "image/earth.bmp";
//
从指定目录装载纹理图像文件
textrue_Resource = auxDIBImageLoad(resource_path);
//
指定该纹理在进行贴图时的属性,GL_LINEAR和GL_NEAREST说明在贴图
//
的时候进行线性插值,使用最近颜色进行匹配,这样将得到比较自然平滑
//
的纹理贴图效果
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,
textrue_Resource->sizeX,textrue_Resource->sizeY,0
,GL_RGB,GL_UNSIGNED_BYTE,textrue_Resource->data);
在程序中加入纹理以后,需要将纹理贴到具体的物体表面,这就要指定纹理坐标。也就是说明纹理图片的那个位置贴到物体的哪个位置,由于本文探讨的主题是光照,所以不对纹理作详细的说明。在程序中使用简单的纹理坐标映射,使用下面的代码可以简单方便的对球体实现纹理贴图:
//
绑定我们之前定义好的纹理
glBindTexture(GL_TEXTURE_2D, textrue_Resource);
//
用于自动纹理映射的二次曲面对象
GLUquadricObj* g_text= gluNewQuadric();
//
绘制半径为0.3的球体,同时使用
gluSphere(g_text,0.3,64,64);
第一条语句将当前纹理设置为我们之前定义好的纹理对象。第二条语句生成一个帮助产生纹理映射坐标的对象,第三条语句绘制一个半径为0.3大小的球体,同时使用GLUquadricObj的对象产生纹理坐标,使用产生的纹理坐标进行纹理贴图。这两个函数时GLUT库里面的函数,在使用的时候需要将GLUT库包含进来。
//
包含GLUT库
#include
#pragma comment(lib,"glut32.lib")
当有两个光源的情况下,再加上纹理贴图后,程序运行的效果如下:
三维太阳系模拟程序(Solar System)介绍
在掌握了光照的基本知识后,可以使用这些知识完成一个简单模拟太阳系的练习程序。加上各种光照效果,纹理贴图以及相应的交互操作处理。这个程序将真正综合运用我所介绍的各种光照处理,以及交互操作,坐标系转换,定时器设置,星球运动轨迹设置,星球运行轨道绘制,公转自转运动实现等内容,可以算一个真正意义的上的一个完整的三维程序,下面我将介绍这个程序的实现和要注意的问题。
下面的代码,可以实现
Z
缓存检测,这样就会先绘制离屏幕远的物体后绘制离开屏幕近的物体,避免被前面挡住的物体也绘制出来;只对外面的多边形进行渲染,对内侧的多边不进行渲染处理,提高程序运行效率;设置投影方式为正交投影,符合我们现实生活观察物体的真实成像方式;设置视角大小,也就是设置可以观察到的视野范围的角度;开启纹理处理,为各个天体贴上纹理贴图增加真实感。如下面的代码:
//
开启深度
Z
缓存
glEnable(GL_DEPTH_TEST);
//
设置只有正面多边形进行光照计算
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_FALSE);
//
设置正交投影
glMatrixMode(GL_PROJECTION);
//
设置视角大小
gluPerspective(45.0,(GLfloat)(SCREEN_WITH)/(GLfloat)(SCREEN_HEIGHT),0.01,10000000000);
//
开启纹理效果
glEnable(GL_TEXTURE_2D);
//
开启纹理映射对象的纹理映射开关
gluQuadricTexture(g_text,GL_TRUE);
综合演示程序的运行效果如下图所示:
该演示程序实现了简单的太阳系模拟,几个主要的行星围绕着中心的太阳进行公转,行星自己也进行着自转,地球的卫星月亮围绕着地球旋转,同时卫星也进行自转。场景中有一个光源--太阳,同时开启了全局环境光线,所以在关闭唯一的光源(太阳)后,这个太阳系统仍然可以看见各个星体,只是非常暗(因为我设置的全局环境光很弱)。程序开始运行的时候,可以选择是在全屏模式或者在窗口模式下运行。程序在运行中可以使用键盘控制观察点进行前后左右的平移动,使用鼠标控制观察点所朝向的方向,操作方式和常见的第一人称射击游戏相同。
演示程序的具体操作如下:
W
向前平移
S
向后平移
A
向左平移
D
向右平移
L
开启
/
关闭太阳光源
鼠标控制方向
退出程序
由于Solar System程序并不算大,所以编程没有使用面向对象编程,而是面向过程的程序。程序中几个主要的函数分别是:WinMain,WndProc,ReSizeOpenGLWindow,RenderOpenGLScene,LoadImageResources,InitialOpenGL这几个函数。
WinMain
是程序的主函数,消息循环在该函数中进行,在没有消息的时候,调用RenderOpenGLScene函数对场景进行渲染。
WndProc
是程序中的消息响应函数,主要是用于响应用户的操作,进行相应的处理,实现用户与程序的交互。
ReSizeOpenGLWindow
是窗口大小改变的时候,重新设置OpenGL的相关属性,以保证程序在窗口大小改变后依然正确的运行。
RenderOpenGLScene
函数是进行具体的场景渲染处理运算。
LoadImageResources
函数装载程序中使用到的各种纹理贴图的图像文件,并设置相应的纹理对象,为绘制场景准备好纹理对象。
InitialOpenGL
是在程序启动OpenGL的时候,对OpenGL进行各种初时化的设置。
Solar System
程序的运行流程如下图所示:
程序整体流程图
由于OpenGL是一个状态机系统,在进行绘图时绘图的效果根据当前的OpenGL的状态属性决定。所以在程序中使用了大量的全局变量,表示不同的状态,方便在程序中切换OpenGL的状态。这些全局变量包括:纹理资源路径、光照效果开关、观察点方向、观察点位置、移动观察点的步长、太阳光源的各种属性、全局光照的属性、行星材质、材质自发光属性、定时器时间等。
程序启动以后,进入WinMain主函数首先要创建窗口,这和常规的Windows应用程序一样,就不多说了。之后要做的是调用InitialOpenGL函数对OpenGL进行初始化。
InitialOpenGL
进行初始化的第一件事是设置显示的象素格式,创建OpenGL的渲染表DC将渲染表DC与当前的程序关联起来,代码如下:
//
设置程序的像素显示格式
if(!SetPixelFormat(hDC,ChoosePixelFormat(hDC,&pixelFormateDes),&pixelFormateDes))
{
MessageBox(NULL,"
设置窗口象素格式失败!","错误",MB_OK);
return FALSE;
}
if(!(hRC= wglCreateContext(hDC)))
{
MessageBox(NULL,"
创建OpenGL渲染表失败!","错误",MB_OK);
return FALSE;
}
if(!wglMakeCurrent(hDC,hRC))
{
MessageBox(NULL,"
关联OpenGL渲染表失败!","错误",MB_OK);
return FALSE;
}
其中pixelFormateDes是描述象素格式的结构体,先填充这个结构体后,然后就可以使用这个结构体进行设置了。如果上面的代码正常执行,则OpenGL就可以在Windows程序中正常运行了。
接下来的就可以进行OpenGL的各种状态属性的设置了。首先设置投影类型,观察视角的大小并开启Z缓存深度测试。实现代码具体如下:
//
设置正交投影
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0,(GLfloat)(SCREEN_WITH)/(GLfloat)(SCREEN_HEIGHT),0.01,10000000000);
//
开启深度Z缓存
glEnable(GL_DEPTH_TEST);
上面的代码中glMatrixMode设置我们进行的投影方式是透视投影方式,glLoadIdentity语句将当前操作的矩阵(也就是透视投影矩阵)初始化为单位矩阵。因为OpenGL是一个状态机,所以每次设置相应矩阵的时候,要记住将其初始化为单位矩阵,避免以前对该矩阵的操作对其产生影响。
投影方式分为两种:透视投影方式和正交投影方式。透视投影方式的范围是一个棱锥台,棱锥台内部区域的物体可以被观察到,这部分物体投影到显示屏幕上,呈现近大远小的效果,这就是我们在日常生活中实际的观察效果相符。另一种投影方式正交所观察的范围是一个立方体,立方体内部的所有物体垂直投影到显示屏幕上,没有近大远小的效果,在机械制图时就是采用的正交投影方式。
gluPerspective
语句设置了在透视投影过程中的视角范围,换句话说就是定义了透视投影中的棱锥台观察区域。上面的语句表示视角沿垂直方向的角度是45度,水平方向的角度是45 ×SCREEN_WITH/SCREEN_HEIGH度,离视点最近距离为0.01,最远距离为10000000000的棱锥台。glEnable(GL_DEPTH_TEST)打开Z缓存,这样OpenGL在绘制物体的时候,会先绘制离观察点远的物体,后绘制离观察点近的物体。这样前面的物体就会挡住后面的物体,否则3D物体前后的遮挡关系会是混乱的。OpenGL状态机在最开始的时候,是默认没有打开Z缓存测试的。
之后我们设置光照计算方式、全局光照属性、打开光照渲染。代码如下:
//
设置只有正面多边形进行光照计算
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_FALSE);
//
全局光照
glLightModelfv(GL_LIGHT_MODEL_AMBIENT , globel_ambient);
//
打开光照渲染处理
glEnable(GL_LIGHTING);
globel_ambient
是在程序中预先定义好的全局变量,glEnable(GL_LIGHTING)语句开启场景的光照处理计算,在默认的状态下关闭的。
Solar System
里面只有一个光源,就是太阳,关于该光源的所有属性变量已经作为全局变量在定义的时候设置好了,在下面的代码中直接使用这些值来指定光源的各项特性,并打开该光源。代码如下:
//
太阳光源
glLightfv(GL_LIGHT0 , GL_AMBIENT , sun_ambient);
glLightfv(GL_LIGHT0 , GL_DIFFUSE , sun_diffuse);
glLightfv(GL_LIGHT0 , GL_POSITION , sun_position);
glEnable(GL_LIGHT0);
上面的代码将各种预先的光源属性的值,赋值给GL_LIGHT0光源,然后开启GL_LIGHT0光源。但是要注意的是,要使得GL_LIGHT0光源产生效果,或者是任何光源产生效果,必须之前要打开OpenGL的光照渲染设置选项,也就是使用glEnable(GL_LIGHTING)语句。
然后需要设置的是星体的材质的属性,和设置光源属性类似。我们使用预先定义好的材质属性的全局变量的值,来对星体的材质进行设置。代码如下:
//
设置星体的材质
glMaterialfv(GL_FRONT , GL_AMBIENT ,planet_ambient);
glMaterialfv(GL_FRONT , GL_DIFFUSE ,planet_diffuse);
glMaterialfv(GL_FRONT , GL_SPECULAR ,planet_specular);
glMaterialfv(GL_FRONT , GL_SHININESS , planet_specular_parameter);
glMaterialfv(GL_FRONT , GL_EMISSION , planet_self_emission);
在光照,投影方式,视角,材质都设置好了以后,我们就可以装载贴图所需要的图像文件,设置纹理对象。这些操作与前面设置光照、材质、视角范围、投影方式等没有前后关联,可以在这些操作以前进行,也可以在之后进行,代码如下:
FILE *File=NULL;
resource_path[0] = "image/sun.bmp";
resource_path[1] = "image/mercury.bmp";
resource_path[2] = "image/venus.bmp";
resource_path[3] = "image/earth.bmp";
resource_path[4] = "image/moon.bmp";
resource_path[5] = "image/mars.bmp";
AUX_RGBImageRec* textrue_Resource[6];
//
装载图像文件资源
for(int i = 0 ; i < 6 ; i++)
{
File=fopen(resource_path[i],"r");
if(!File)
{
MessageBox(NULL,"
加载图像资源文件失败 !","Fail",MB_OK);
return FALSE;
}
fclose(File);
textrue_Resource[i] = auxDIBImageLoad(resource_path[i]);
File = NULL;
}
上面的代码中resource_path是一个全局变量,用于存储图片文件目录的数组(char** resource_path = new char*[5])。在Solar System中一共有6个星体(包括恒星、行星和卫星),所以一共有6个贴图文件。AUX_RGBImageRec是glaux库中的一个结构体,该结构体记录装载到程序中作为纹理贴图的图片的信息,帮助我们产生纹理对象。auxDIBImageLoad也是glaux库中的一个函数,它从指定目录转载图像文件,返回一个描述并记录了该图像文件信息的AUX_RGBImageRec结构体。
将用作纹理贴图的图像文件转载到程序中后,就可以用于产生相应的纹理对象了,进行处理的代码如下:
//
生成纹理
glGenTextures(6,textrue);
for(i = 0 ; i < 6 ; i++)
{
glBindTexture(GL_TEXTURE_2D,textrue[i]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,
textrue_Resource[i]->sizeX,textrue_Resource[i]->sizeY,0
,GL_RGB,GL_UNSIGNED_BYTE,textrue_Resource[i]->data);
//
删除堆上的临时图像
delete textrue_Resource[i]->data;
delete textrue_Resource[i];
}
上面的代码中的glGenTextures函数的作用是产生特定个数的纹理对象,并生成代表每个纹理对象的整型数字。上面该函数调用的语句,产生了6个纹理对象,并且每个纹理对象有产生一个对应AUX_RGBImageRec结构体的数据设置相应的纹理贴图对象,glBindTexture通过textrue整型数组指定进行操作的纹理对象,glTexParameteri函数设定纹理对象对应的纹理在进行贴图处理时的处理方式,上面的代码指定了进行线性插值并且使用最临近象素的处理方式,这样贴图在放大缩小的时候将会得到很平滑自然的视觉效果。glTexImage2D是设置纹理对象的关键语句,它根据AUX_RGBImageRec结构体所记录的图像文件的信息,设置相应的纹理对象。设置完成后,AUX_RGBImageRec结构体就没有作用了,我们将直接使用纹理对象,所以我们将堆内存上的AUX_RGBImageRec结构体所占用的内存释放。
完成了这些所有的设置后,程序进入消息循环,当有消息的处理消息,没有消息的时候,进行OpenGL场景渲染。代码如下:
//
程序主循环
while(true)
{
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if(msg.message == WM_QUIT)
break;
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
RenderOpenGLScene();
}
}
在上面的代码中获取程序消息,我们使用PeekMessage而不是GetMessage。因为GetMessage是阻塞方式的函数,调用该函数去获得程序的消息时,如果程序当时没有消息产生,则该函数将停在这里直到获得消息时才返回,继续后面的程序语句。PeekMessage在去获得消息时,如果发现消息队列没有消息就马上返回。这点区别很重要,这样在没有消息时,程序就可以把时间画在渲染OpenGL场景上。如果有消息的话,程序将消息抛给相应函数WndProc,进行用户消息响应,本程序中主要是用户的键盘鼠标操作响应和定时器响应。
对于消息响应函数WndProc处理三类消息,一类是与用户鼠标键盘输入控制视点位置方向相关的消息,一类是在窗口模式下改变窗口大小的消息,最后一类是定时器的消息。对于改变窗口大小时,调用ReSizeOpenGLWindow函数重新设置OpenGL的视角范围的属性,代码如下:
if(height == 0)
height = 1;
//
设置视口
glViewport(0,0,width,height);
//
设置视野角度范围
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0,(GLfloat)(width) / (GLfloat)(height) , 0.01 ,10000000000);
上面的代码中,height和width是改变窗口大小后,新窗口的大小。为了避免0作为除数出现异常,所以要保证height不为0。glViewport设置进行透视投影的大小范围。gluPerspective根据窗口新的大小设置透视棱锥台,如果不作这样的处理,则窗口改变大小的时候,图像将发生变形出现不真实的效果。
消息响应函数中的定时器消息,是用于实现Solar System中行星运动,当到某个时间的时候,星体移动到程序所期望的位置。程序根据当前经过的时间,计算各个星球所在的位置。
对于用户的键盘鼠标控制视点位置朝向的消息,程序更新当前视点所在的位置和朝向。在渲染函数RenderOpenGLScene中根据这些值渲染场景。而视点的位置和朝向是作为全局变量,RenderOpenGLScene直接使用这些全局变量进行绘制,由于代码行数较多,所以只列几行代码说明:
//
向左
case 'A':
view_position[0] += sin((horizontal_angle + 90.0) * 3.14159265 / 180.0);
view_position[2] += cos((horizontal_angle + 90.0) * 3.14159265 / 180.0);
上面代码对于按下A键(向左移动)进行处理,view_position[0]和view_position[2]是记录视点的坐标位置的全局变量,horizontal_angle是记录当前视点朝向水平角度的全局变量。上面语句将视点沿视点当前朝向左平移,在渲染函数中将使用这些全局变量进行绘制。
在渲染函数RenderOpenGLScene中对整个太阳系进行绘制,包括所有星体,轨道以及设置视点位置与朝向。下面的代码对太阳进行绘制:
//
重置ModelView矩阵
glMatrixMode(GL_MODELVIEW);
//
绘制太阳
glPushMatrix();
glMaterialfv(GL_FRONT , GL_EMISSION , sun_self_emission);
glBindTexture(GL_TEXTURE_2D,textrue[0]);
gluSphere(g_text,3.0,128,128);
glLightfv(GL_LIGHT0 , GL_POSITION , sun_position);
glMaterialfv(GL_FRONT , GL_EMISSION , planet_self_emission);
RenderOpenGLScene
函数会被频繁的调用,每次调用都会对模型视点矩阵进行修改,要避免上次的修改对其造成影响,所以使用glPushMatrix函数把当前的模型视点矩阵压入堆栈保存,当绘制完成后再弹出堆栈。我们在初始化OpenGL的时候,已经设置了材质自发光属性,但是太阳的自发光应该很大,所以在这里要再次修改该属性,在绘制完太阳后再修改回来。glBindTexture绑定指定的纹理对象,gluSphere根据绘制球体并使用指定的纹理对象进行贴图。太阳光源位于太阳的中心点,glLightfv设置该光源的位置。其他的星体绘制和太阳绘制类似,详细内容请参见源代码,代码中有详细的注释。
当绘制完所有的星球后,还要绘制行星的运行轨道。代码如下:
glDisable(GL_LIGHTING);
glDisable(GL_LIGHT0);
glDisable(GL_TEXTURE_2D);
//
绘制地球轨道
glBegin(GL_LINE_LOOP);
for(int i = 0 ; i < 100 ; i++)
{
glVertex3f(
25 * sin(2 * 3.14159265 * i / 100) ,
0.0 , 25 * cos (2 * 3.14159265 * i / 100)
);
}
我们在前面提到OpenGL是一个状态机,我们之前开启了光照和纹理贴图效果,所以绘制出的星球将进行光照计算和纹理贴图计算。而行星运行轨道只是几条比较暗淡的线,我们不需要轨道产生光照和纹理计算。所以glDisable语句关闭相应的效果,对于圆形的轨道使用折线进行逼近。
所以的绘制完成以后,应该设置视点的位置和朝向,这些信息在WndProc的相应的消息处理过程中已经计算好了,并且保存到了全局变量中,现在我们只需要使用这些变量来进行设置就可以了,下面是其实现代码:
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
GLdouble X = view_position[0] + sin(y_axis_angle / 90) * sin(horizontal_angle / 90);
GLdouble Y = view_position[1] + cos(y_axis_angle / 90);
GLdouble Z = view_position[2] + sin(y_axis_angle / 90) * cos(horizontal_angle / 90);
gluLookAt(
view_position[0] , view_position[1] , view_position[2] ,
view_position[0] + sin(y_axis_angle * 3.14159265 / 180.0) * sin(horizontal_angle * 3.14159265 / 180.0) ,
view_position[1] + cos(y_axis_angle * 3.14159265 / 180.0) ,
view_position[2] + sin(y_axis_angle * 3.14159265 / 180.0) * cos(horizontal_angle * 3.14159265 / 180.0),
0.0 , 1.0 ,0.0 );
上面的语句中glMatrixMode指定当前对模型视点矩阵进行操作,glLoadIdentity对该矩阵进行归一处理使其变为单位矩阵,避免上次的调用对其有所影响。X,Y,Z三个变量表示视点的坐标位置。gluLookAt设置视点的位置和朝向。
至此,Solar System主题部分已经全部说明完毕。
参考资料
《
OpenGL
编程指南(第四版)》
人民邮电出版社
《计算机图形学》
电子工业出版社
《交互式计算机图形学》
高等教育出版社
《计算机图形学实验教程》
机械工业出版社
《
3D
游戏编程》
中国科技出版社