一、Nehe的内容
我们先来看Nehe教程中的内容。在这一课,个人认为最重要且难理解的内容主要有三个,分别是对面的设置处理、读入并处理文件及数据、设置世界的移动旋转。
1、面的构成
从代码的实现来说,这个问题并不难,重要的是这个想法。从前面的几课中我们可以看出,简单一些的立体图形都可以通过一个个面来构成,因此在我们画立方体的时候是通过面的拼接,同样的,在构建一个3D世界时,我们也可以通过一个个小的面来拼出来。这样就产生了一个想法,我们先构建一个顶点结构,一个顶点有五个数据,X轴,Y轴,Z轴和两个纹理坐标,再构建一个三角形结构,一个三角形有3个顶点,最后为我们的世界构建一个区段结构,“每个3D世界基本上可以看作是sector(区段)的集合。一个sector(区段)可以是一个房间、一个立方体、或者任意一个闭合的区间。”这是Nehe教程中的定义。区段中包含了基本图形即三角形的个数以及指向三角形数组的指针。这样一个3D世界的层次就构建好了。如下。
typedef struct tagVERTEX // 创建Vertex顶点结构
{
float x, y, z; // 3D 坐标
float u, v; // 纹理坐标
} VERTEX; // 命名为VERTEX
typedef struct tagTRIANGLE // 创建Triangle三角形结构
{
VERTEX vertex[3]; // VERTEX矢量数组,大小为3
} TRIANGLE; // 命名为 TRIANGLE
typedef struct tagSECTOR // 创建Sector区段结构
{
int numtriangles; // Sector中的三角形个数
TRIANGLE* triangle; // 指向三角数组的指针
} SECTOR; // 命名为SECTOR
但一个3D世界很复杂,即便是简单的3D世界,也要用到十几个甚至更多的面,这意味着我们要打印十几次相同的代码,而它们仅仅是坐标参数略有不同。如果可以把坐标都存到某个数组里,然后用循环逐次读入,这样会轻松一些。但这时就产生了新的问题,一个点的坐标有五个数据,画一个三角形要用3个点,一个正方形要用4个点,而一个简单的3D世界,比如Nehe的这个,要用36个三角形(即108个点),换成正方形则是18个(即72个点),我们可以看得出来,这个数据太大了。因此,我们考虑把数据存到一个文件中,要用的时候逐一读取。这就是接下来的文件内容。
2、文件读取和处理
在这里,我们把数据存在txt文档中。第一行是文档中三角形个数,或者说程序运行时读取的三角形个数,如果你写了一百个三角形的数据,但只用到前30个,就可以把这个数字写成30。接下来就是一组一组的三角形数据了。
首先,要从文件中读取完整的一行数据存到一个数组里,在这里用readstr这个函数,如下。
void readstr(FILE *f,char *string)
{
do
{
fgets(string, 255, f);
} while ((string[0] == '/') || (string[0] == '\n'));
return;
}
这个函数有两个参数,第一个是读取的文件,第二个是存数据的数组。fgets函数是从文件中读取完整的一行并存入数组,关于这个函数的具体内容参见百度百科。可以看出,在while循环中,把读到的内容都存到string中,如果遇到注释的标志’/’或换行的标志’\n’,就重新开始循环,即换行时就重新开始存。这样很巧妙地避开了文档中的注释语句和空行。
接下来,就是如何打开并读取文档从而设置世界的问题了。在SetupWorld函数中,用这样一个函数打开了文档,filein = fopen(“world.txt”, “rt”);然后用前面的readstr函数把文档的数据以行为单位提取到数组oneline中,把第一行中的三角形个数存到变量中,再用for循环将剩下的数据存到顶点数组中。for循环有两层,外层是个数循环,即从第一个三角形开始一直到最后一个,内层是顶点循环,即从第一个顶点到第三个定点,这样,每一个定点就以如下的方式存到了前面提到的结构体中。
sector1.triangle[loop].vertex[vert].x = x;
sector1.triangle[loop].vertex[vert].y = y;
sector1.triangle[loop].vertex[vert].z = z;
sector1.triangle[loop].vertex[vert].u = u;
sector1.triangle[loop].vertex[vert].v = v;
最后,读取完数据我们关上这个txt文档, fclose(filein);
接下来要做的就是用这些数据绘制一个个面了。绘制没有什么难度,重要的是旋转。
3、世界移动旋转
在这一课,世界旋转和前进后退和前面变得不一样了,因为3D世界要有一个轻微的摇摆效果,这样是模拟人走在路上时头部的轻微摆动。同时,在这里加入了世界的旋转,即以视点为中心旋转整个世界。我们采用的方法是:根据用户的指令旋转并变换镜头位置;围绕原点,以与镜头相反的旋转方向来旋转世界。(让人产生镜头旋转的错觉);以与镜头平移方式相反的方式来平移世界(让人产生镜头移动的错觉)。
//前进
xpos -= (float)sin(heading*piover180) * 0.05f;
zpos -= (float)cos(heading*piover180) * 0.05f;
if (walkbiasangle >= 359.0f)
{
walkbiasangle = 0.0f;
}
else
{
walkbiasangle+= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
//后退
xpos += (float)sin(heading*piover180) * 0.05f;
zpos += (float)cos(heading*piover180) * 0.05f;
if (walkbiasangle <= 1.0f)
{
walkbiasangle = 359.0f;
}
else
{
walkbiasangle-= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
//左转
heading += 1.0f;
yrot = heading;
//右转
heading -= 1.0f;
yrot = heading;
在这一块,Nehe的教程讲的很清楚。
值得注意的一点是,我们在处理图像坐标和坐标之间的关系以及一些旋转等问题时,sin和cos这两个函数往往十分有用,比如说让物体绕某点圆周运动,如果用圆的方程那个开根号的方式来写y关于x的变化,会发现圆周运动速度变化很明显,而如果用sin和cos把x和y写成关于一个角度变量的函数,就可以得到匀速圆周运动,并且避免了对x和y正负的判断等问题。
二、我的增加与修改
Nehe的内容是一个简单3D世界的基础,在这个基础上我做了一些很小的改进,也算是对这一课的复习。
1、和第九课的结合
第九课移动图像是一个旋转的星星,把这两课的内容结合在一起,主要就是把InitGL和DrawGLScene这两个函数的内容结合起来,然后在主函数的按键控制部分加入需要的内容。要注意的是,第九课的混色要关闭深度测试,而第十课的混色要打开深度测试,而且第九课的混色是一直开着,而第十课的混色是按B键后打开。关于这个的设定可以看 http://blog.youkuaiyun.com/u014420201/article/details/44064737 。其他的就没什么了。
2、三角形到正方形的转变
大多数时候,我们会用三角形来构成3D世界,这主要是因为由三个给定顶点构成的三角形一定共面,而四个顶点得到的四边形却不一定共面,不共面的四边形会导致纹理渲染出错。但在简单的3D世界里,或者说很明显都是平面的世界里,我们可以构建四边形结构体,把后面涉及三角形的地方都改成四边形,同时把坐标从三角形改成四边形。这个改动并不是很难,要注意的主要就是纹理坐标的顺序,以及代码中改动的地方不要有遗漏。我把要改的列出来,如下。
a.创建结构时,要把顶点数从3改成4,同时下面的区段结构中的变量名为了方便也最好改了;
b.SetupWorld()函数中的for循环,顶点循环的那层也要从3改成4;
c.DrawGLScene()函数中的for循环,同样要加一个顶点的设置,并且要注意改了的变量名。
3、自己设置坐标
在熟练地实现了教程中的3D世界后,我们会想要自己设计一个世界,虽然复杂的世界比如高山、城市、迷宫等等都不可能这么容易地就设计出来,但设计一个简单的只是有几面围墙几块地板的世界还是可以的。在这里要注意的是坐标的顺序,最好都按照顺时针或都逆时针。只要把World.txt文档中的坐标数据改了就够了,整个程序完全不用修改。
4、键盘控制的增加
最后,我增加了一些键盘控制,首先是前进后退用W和S控制,左右旋转视角用A和D来控制,向上向下看用M和N控制,左右移动和上下移动用四个方向键来控制,这样就可以实现在整个3D世界的漫游。
在这里还可以做进一步的改进,就是加入鼠标控制,鼠标直接控制视角的旋转移动等,这个问题可以以后探索。
最后,这四个改动后的代码在: http://download.youkuaiyun.com/detail/u014420201/8540581 。