放假啦放假啦放假啦!!!回家啦回家啦回家啦!!!
0x00 引言
这学期计算机图形学课程的一项大作业,前半学期一直在自学OpenGL的一些技术,为了大作业做铺垫,学习之余总结了一些技术要点,给自己也是供组员学习。见下面链接:
https://www.jianshu.com/p/19fc16664428
最终完成了一个简陋的天安门广场的模型设计,用到光照、材质、纹理等一些技术来增强真实感效果(虽然还是很不真实)。在自学过程中,通过许多博主优秀的文章学习到了很多知识,这里也非常感谢大家的分享,因此我也将本程序分享出来,给那些只用到glut工具包来编程序的人提供一些帮助。
ps:程序有许多不足,大家自行鉴定。
ok,开始搬移实验报告!
0x01 问题描述
设计天安门广场的3D景物模型,建立形体的参数模型。采用图形的几何变换方法将所定义物体的投影图绘制出来,再使用光照和纹理等技术,增加其真实感效果。
景物模型中可以设计有多种元素,尽量使设计出的场景与真实模型匹配,以丰富实验效果。本实验中设计有如下模型:
建筑模型:天安门、人民英雄纪念碑、毛主席纪念堂、中国国家博物馆、人民大会堂、正阳门。
移动模型:太阳、月亮、小人游客和雪花。
装饰模型:花坛、树木和告示牌。
其他模型:马路和旗杆。
在具有真实感的场景中,除了应该设计模型以外,还应该对场景和模型加以处理,以增强它的真实感,赋予它生命。本实验中用到了如下技术完善场景效果:
光照模型,材质属性,纹理贴图,天空盒。
由于实现的场景比较大,仅在不变的窗口中是不能显示完整的场景的,因此为了使用户和编程者能够观察整个场景,需要给程序添加场景漫游功能——可以通过键盘或鼠标控制程序,任意地移动视角。
0x02 详细设计
1.光照系统
openGL在处理光照时把光照系统分为三部分,分别是光源、材质和光照模型。下面会分别介绍我们程序在光照系统的设计。
(1) 光照模型
指定全局环境光:
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient_intensity);
(2) 光源
设置一个点光源。光源中加入了漫反射光,它的效果是:物体的某一部分越是正对着光源,它就会越亮,因此漫反射在光源的效果中最明显。
(3) 材质
OpenGL 用材料对光的RGB的反射率来近似定义材料的颜色。材质颜色也分为环境、漫反射和镜面反射成分,颜色参数表示对该种色光的反射程度,1表示完全反射,0表示完全吸收。
在本程序中虽然编写了设置材质的函数,但是并没有启用,原因是目前光照还没有设置好,因此材质也就还没有设置。所以对于本程序的场景效果而言,每一个模型的材质是一样的,因此他们在光照下的效果是一样的。
(4) 颜色跟踪
由于设置材质时,材质会被保存在状态里面(就像设置颜色一样),所以如果不改变材质的话,默认会用最近用过的材质。而一旦开启了材质,就无法关闭,对于某些不需要设置的地方就会影响它(模型)原本的颜色,比较麻烦。颜色跟踪可以解决这一问题:
glEnable(GL_COLOR_MATERIAL);
glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
启动颜色跟踪之后,我们就可以像以前一样,使用glColor函数来指定图元的颜色了。
(5) 光源移动
既然是由日月作为点光源发出光线,那日月在移动的同时,也应该设置光源的位置随之移动。光源动态移动之后,可以很清楚地看到场景表面光照效果的一个变化。
light2_position[0] = sunRadius * cos(timer);
light2_position[1] = sunRadius * sin(timer);
glLightfv(GL_LIGHT2, GL_POSITION, light2_position);
除此之外,在实际生活中,光强度(光照颜色)也是在变化的。比如早上要暗一些,中午达到最亮,下午的时候甚至可能会偏红。所以在程序中我们不仅动态移动了光源,还动态地改变了光线颜色(仅漫反射)。
if(timer <= pi){
rr = sunshine_mat[0] - cos(timer);
gg = sunshine_mat[1] - cos(timer);
bb = sunshine_mat[2] - cos(timer);
change_light[0] = rr;
change_light[1] = gg;
change_light[2] = bb;
}
glLightfv(GL_LIGHT2, GL_DIFFUSE, change_light);
2.纹理
一般开启纹理,都是使用纹理贴图将图片映射到指定的图形上,以显示更好的模型效果,下面是本程序在进行纹理贴图时的步骤:
1.启用2D纹理:glEnable(GL_TEXTURE_2D);
2.加载纹理图像(即导入图片):GLuint texName=load_texture();
3.创建纹理对象:glGenTextures(1, &texName);
4.绑定纹理对象:glBindTexture(GL_TEXTURE_2D, texName);
5.设定纹理过滤的参数:glTexParameteri();
6.为纹理对象指定纹理图像数据:glTexImage2D();
7.纹理映射,绘制纹理图像:glTexCoord2f();
因为纹理贴图时所用的代码和函数较多,特别是在设定纹理过滤时,需要设置很多参数,其实我们也没有把每个函数和参数都研究的特别明白,仅仅是了解了纹理贴图的过程并得到了看着还不错的效果,纹理还有更多有用和复杂的应用,等待着我们以后去学习和使用。
3.场景漫游
在讲解场景漫游前,需要先介绍一下OpenGL的矩阵模型变换。用glMatrixMode函数来设置当前操作的矩阵的模式。
投影变换:glMatrixMode(GL_PROJECTION);
投影变换就是定义一个可视空间,可视空间以外的物体不会被绘制到屏幕上。即将3D变换成2D的一种变换模式。它有两种类型的投影变换,分别是透视投影和正投影。
正投影相当于在无限远处观察得到的结果,它只是一种理想状态,缺乏立体透视效果,本程序没有使用到。
透视投影是视点在有限距离处的投影模式,它所产生的结果类似于照片,有近大远小的效果。
在OpenGL中用gluPerspective函数来设置在窗口中所观察到的内容。这个函数一般设置一次就不再改变了。
模型变换:glMatrixMode(GL_MODELVIEW);
在这种模式下,可以改变观察点的位置与方向和改变物体本身的位置与方向。
改变物体的位置和方向主要涉及以下三个函数:
glTranslate(),glRotate(),glScale()
第一个函数在本程序中多次使用,它可以改变物体的位置。
改变观察点的位置主要使用:gluLookAt(),他可以设置观察点(相机)的位置和观察目标的位置。
接下来说一下我对场景漫游的理解:场景漫游就是通过鼠标或键盘的操作,改变视角范围,调整在OpenGL窗口中显示图像的内容,以使观察者可以获取到一个3D场景的全部内容。在投影变换中说到,用gluPerspective函数设置我们可以看到的一个“近大远小”的场景,通过调整参数可以设置出一个合理的观察视口。这个设置是不改变的,即在这一视角下能观察到的内容是不全面的,因此可以转到模型变化下,使用gluLookAt()调整观察点的位置以观察到更多的图像内容。
gluLookAt()有9个参数,分别是3对坐标,第一对是人眼(相机)位置。第二对表示眼睛“看”的那个点的坐标,可以理解为视线方向。最后一对表示观察者的方向,设置为正向观察即可。在程序中我们是这样设定的:
float cx = 0.0f, cy = 15.0f, cz = 80.0f;//相机位置
float lx = 0.0f, ly = 0.0f, lz = -20.0f;//视线方向,初始设为沿着Z轴负方向
gluLookAt(cx, cy, cz, cx + lx, cy + ly, cz + lz, 0.0f, 1.0f, 0.0f);
我们将视线方向设置为向屏幕内看去,知道了改变观察点位置的函数,我们就可以写出响应键盘和鼠标的函数,根据相应的指令去修改gluLookAt函数中的参数即可完成视角范围的变换。
相机和视线同时移动:平移相机改变其位置,视线方向不改变。有六个方向可以移动,分别是前、后、左、右、上、下,对应着改变cx、cy、cz的值即可。由于lx、ly、lz不改变,因此是只移动相机,不改变视线方向。
仅视线移动:相机位置不变,改变视线方向。可以左右旋转360度,也可以上下移动相机角度。按照三角函数计算,对应着改变lx、ly、lz即可。由于cx、cy、cz不改变,因此是相机位置不变,只转动相机镜头。
键盘响应
按下不同的键,相应地改变gluLookAt函数的参数。由于每次按下是修改一个定值,在视角变化时并不灵活,所以编写了鼠标响应函数。
鼠标响应
可以检测到鼠标左键还是右键按下了,每当有键按下时就记录当前的鼠标坐标。当鼠标移动时,实时记录鼠标新的位置的坐标,将鼠标按下时的坐标与实时移动时的坐标相减,可以得到在屏幕x和y轴上的一个差值。根据这个差值去调整gluLookAt函数的参数,因为视角的调整是跟随鼠标移动而改变的,因此视角范围变化时会比键盘控制显得更加流畅,灵活自然。
本程序设计了鼠标左键响应和鼠标右键响应。左键点击并移动,控制视线方向。右键点击并移动,控制相机位置。
4.矩阵的入栈和出栈
一般有两种方法对模型的位置进行处理,一是在建模时就指定好坐标;二是在原点画出模型,再通过移动函数将模型移动到指定位置。
对于第二种思路,移动之后需要注意,矩阵的上一次的变换结果对本次变换有影响,上次modelview变换后物体在世界坐标系下的位置是本次modelview变换的起点。所以如果不进行处理就开始绘图,或是移动位置,它的基点是在上次移动后的位置,而非原点。解决方法是使用glPushMatrix()和glPopMatrix()进行矩阵的压栈和弹栈。
当做了一些移动或旋转等变换后,使用glPushMatrix(),OpenGL会把这个变换后的位置和角度保存起来。然后你再随便做第二次移动或旋转变换,再用glPopMatrix(),OpenGL就把刚刚保存的那个位置和角度恢复(比如原点位置)。
即本次需要执行的缩放、平移等操作放在glPushMatrix和glPopMatrix之间,glPushMatrix()和glPopMatrix()的配对使用可以消除上一次的变换对本次变换的影响。
在程序设计中,经常会调整模型的位置,以达到更合适的布局和效果,所以每次要进行modelview变换时,都需要在glPushMatrix和glPopMatrix之间。除此之外,在本程序中的雪花模型与小人模型中会更多次的用到,因为程序需要控制多个这样的动态模型,在每一时间t内,模型都需要移动,因此必须计算好每次移动后的坐标,在glPushMatrix和glPopMatrix之间进行模型的移动,这样后续的模型才是在原点位置操作,才能移动到正确的位置。
5.雪花模型
结构体:位置,颜色,大小,速度,加速度,存在时间,消失速度。
原理:在正方形上贴雪花图,设置混合因子获得半透明效果。
生成位置:场景上方同一高度y,随机x、z坐标初始化。
移动:给定xyz轴上的速度,雪花按照此速度在每一时间t内向三个方向移动。y轴向下加速移动(每一时间t内进行一次加速),x和z轴上随机方向移动,即雪花按照一个角度倾斜移动。
消失并复位:检测到雪花落到地上,即y轴坐标小于或等于0。或是生存时间到了(避免速度太小,落不下来),就重新初始化雪花的位置,即回到初始高度。
6.小人模型
结构体:移动时间,xyz轴上的坐标,旋转角度。
原理:此模型由6个立方体块构成,分别是头,身体,腿和胳膊。当小人移动到指定位置时,调用画小人的函数,将小人模型绘制在此位置。
生成位置:y轴在场景的地面上,随机的x、z坐标。
移动:按照当前的方向,移动一段距离,当到达移动时间后,给小人一个随机角度,这样小人下次就会按照新的方向移动,即实现了小人的转向。
7.天空贴图
问题:天空盒是一个立方体贴图,但如果图片处理的不好,在立方体每个面的交接处会有一道缝隙。除此之外,为了让立方体的六个面显示的图像能够连接在一起,而不是6张一样的图显示在6个面上,需要对原贴图进行特别的处理。这两个问题导致我们无法构建一个自然、效果不错的天空盒。
思路改进:我们将立方体改为了一个球体,因为球体贴图时,不需要进行指定坐标位置的纹理映射。所以原本的一张图片映射到球面上,只是将图片上的元素拉伸了,并不像6张图片组合在一起那样分割了图像元素。且在处理图像的时候比较容易,只需要将图像的底色设置为同一颜色,不会出现图像边界在相接时的缝隙问题。
下面是创建一个球体并贴图的代码:
skySphere = gluNewQuadric();//申请二次曲面空间
glBindTexture(GL_TEXTURE_2D, texBeiJing);
gluQuadricTexture(skySphere, GL_TRUE);//纹理函数
gluSphere(skySphere, groundSize*2, 80, 80);
效果改进1:在场景漫游中需要移动相机位置,在移动时会离天空球体或近或远,且一直向一个方向移动会移出天空球体外。在实际场景中天空应该离我们很遥远,因此在相机移动时,不应该出现与天空的距离拉近或拉远的效果出现。即天空应该一直在视线的最远处,不会随着相机的移动而与我们产生距离的变化。因此在场景漫游中,需要加入这样的功能:在移动相机时,相应地移动天空球体的位置,使天空离我们的距离永远是不变的。
效果改进2:由于光照对纹理贴图的影响很小,在昼夜交替时,紧靠光照无法实现我们想要的整体环境颜色的明暗变化(针对天空)。因此我们用PS软件处理了多张天空贴图,做出不同的明暗效果来,在时间函数的变化中,不断改变贴图内容,去模拟天空明暗变化的效果。
8.基础图形绘制
在本程序的各种模型建立中,立方体块画的次数是最多的,画一个立方体需要画6个面,每个面都需要根据4个点的坐标连线生成。所以如果每次画立方体都要写一遍代码会非常麻烦,因此我们写了一个画立方体块的函数,它的代码和思路是这样的:
void constract(double x, double y, double z, double x1, double y1, double z1) //长方体 {
fang[0][0] = x; fang[0][1] = y; fang[0][2] = z; // 第0个点
fang[1][0] = x; fang[1][1] = y; fang[1][2] = z + z1; // 第一个点
fang[2][0] = x + x1; fang[2][1] = y; fang[2][2] = z + z1; // 第二个点
fang[3][0] = x + x1; fang[3][1] = y; fang[3][2] = z; // 第三个点
for (int i = 0; i<4; i++) // for()循环来完成其余的四个点
{
for (int j = 0; j<3; j++){
if (j == 1) fang[i + 4][j] = fang[i][j] + y1;
else fang[i + 4][j] = fang[i][j];}}
}
void build() //和constract一起用,画长方体
{
glBegin(GL_POLYGON);
glNormal3f(0.0, -1.0, 0.0);
glVertex3f(fang[0][0], fang[0][1], fang[0][2]);
glVertex3f(fang[1][0], fang[1][1], fang[1][2]);
glVertex3f(fang[2][0], fang[2][1], fang[2][2]);
glVertex3f(fang[3][0], fang[3][1], fang[3][2]);
glEnd(); // 下底
……
}
在画立方体时,需要给出两对坐标,第一个是立方体的前一面的左下角的点的坐标,第二个是他到后一面的右上角的点的偏移量。即只需要知道两个点的坐标,就可以得到立方体的长宽高,并写出其余6个点的坐标。
获取立方体的8个顶点坐标后,就可以分别画出6个面了。画四边形时可以用glBegin(),通过指定参数告诉接下来想要画什么图形,使用glVertex()来表示多边形的一个坐标。当给出所有的点后,OpenGL会根据参数里的图形把点连接起来形成一个图形。
按照此方法,还可以封装三棱柱,四棱台和圆柱体的绘制过程。
0x03 模型建立
这里感谢小伙伴们画的模型草图以及标注的坐标位置。
编写完基础图形的绘制,设计模型的样子、大小、尺寸和在场景中的位置,按照坐标绘制基础图形图形,完成模型的组合。
1.天安门
底座:立方体+门洞贴图+毛主席头像贴图+文字贴图。
主层:立方体+门窗贴图。
棱台屋檐:四棱台。
立方体屋檐:立方体+国徽贴图。
三棱柱屋檐:三棱柱。
柱子:10个圆柱。
小红旗:6个小红旗。圆柱旗杆+红旗贴图。
2.其他建筑
其他建筑做的比较简陋哈哈。
3.日月模型
4.装饰模型
5.其他模型
0x04 效果展示
0x05 缺点不足
前面也说道,希望大家自行鉴定程序的不足之处,可以进行合理的借鉴及参考。这里列一下程序的不足之处,但是不展开说了。
建筑细节:完善其他建筑细节。
粒子系统:雪花不完全透明。
光照系统:简单,无材质,对纹理无影响,光强及位置变化扯淡,无阴影。
场景漫游:视角上下移有问题。
日月运动轨迹:简单。
天空盒:不好看,方法不好。
碰撞检测:没有。
0x06 改进空间
除了需要改进缺点不足之外,还可以让程序更有意思一点。
1.阴影
作为模拟真实世界的3D场景,怎么能少得了阴影呢,但是我们的程序还真就缺少了阴影效果。最好是可以给模型加上阴影效果,赋予它生命,而不是没有影子的不real的模型。
2.太阳光晕
光晕就是光线的“可视化”,在我们程序中是看不到光线的,只能看到光线照射到模型上产生的影响。而在真实世界中,人直视太阳时,会闪瞎到你的狗眼,我们要做的就是把这个光芒显示出来,增强太阳在场景中的真实感。
3.飘起来的红旗
静态的红旗看起来十分的僵硬,因为它不比其他的模型,它的体质是很轻的,这就注定了它应该是一个随风飘扬的形象。增加这一功能,让场景显得生动起来。
4.积雪效果
虽说本程序加入了雪花效果,但是雪落到地上迅速就“化掉了”,显然不够真实。程序最好是能让落下来的雪花贴图在地面或是建筑上停留一段时间(设定消失时间),当雪下的大时就能形成积雪的效果,是不是又真实又美妙呢。
同样,既然可以造出雪花,那也能模拟出雨滴,如果还有机会,我们预计会增加下雨的效果,增加雾化的效果,且最好是能在场景中形成小水坑的真实效果。
5.汽车
既然本程序都加上了游客系统,为何不考虑一下长安街上行驶的汽车呢。如果还有机会,我们预计会增加汽车功能,构建一个汽车模型,让他行驶在马路上,最好可以看到轮子转动的效果,并且可以自动转向拐弯。
0x07 环境配置
Codeblocks软件。
OpenGL工具包中的glut库。
参考链接:
https://blog.csdn.net/Jason_TBWH/article/details/102510789
0x08 结语
我们组最开始做的时候也没有想到能做成现在这样看着还不错的结果,场景中的很多元素都是慢慢添加的,随着对OpenGL的慢慢熟悉,随着对模型设计和建立慢慢地得心应手,当你开始认识到自己可以依靠自己掌握的技术做出什么东西时,你就会迫不及地去实现它,并构想更多的你想做的东西,在克服困难慢慢地实现你所构想的东西的过程中,你会发现你学到了很多东西,并会构想更复杂、更有意思的东西。
虽然是用的史前技术做的小破程序。
最后再感谢一下小伙伴们的帮助!
操蛋的一学期最终还是结束了,总感觉自己越发的弟弟了。
下载链接:https://download.csdn.net/download/Jason_TBWH/12099264
GitHub链接:https://github.com/BlueWalking/Tiananmen-Square-3D.git