计算机图形学期末作业

作业要求:

期末大作业 虚拟场景建模

一、作业内容

在屏幕上显示一个包含多个虚拟物体的虚拟场景,并且响应一定的用户交互操作。

 

具体内容包括:

  1. 场景设计和显示

学生可以通过层级建模( 实验补充1和2)的方式建立多个虚拟物体,由多个虚拟物体组成一个虚拟场景,要求在程序中显示该虚拟场景,场景可以是室内或者室外场景;场景应包含地面。

  1. 添加纹理

参考实验4.1,为场景中至少两个主要物体添加纹理贴图 。

  1. 添加光照,材质,阴影效果

参考实验3.3和实验3.4,实现光照效果,阴影等。

  1. 用户交互实现视角切换完成对场景的任意角度浏览

参考实验3.1,完成相机变换。

  1. 通过交互控制物体

参考实验2.3,实现物体的变换,允许用户通过键盘或者鼠标实现场景中至少两个物体的控制(移动,旋转,缩放等等)。

 

二、作业说明与要求

  1. 程序代码:本次作业不提供参考代码,具体可参考实验补充2的代码框架,程序运行窗口标题设为“学号_姓名_期末大作业 。最终提交的代码中与作业内容相关部分必须写上注释。 
  2. 过程说明报告:使用附件提供的以论文、报告等形式考核专用答题纸.doc”撰写,题目可以是虚拟场景建模或者场景模型的名称,内容需对整个工程代码实现的过程文字进行详细地描述配有一定的截图说明,即类似于实验报告中实验步骤部分的撰写方式。排版要整齐,字体要规范。宋体五号,至少八页。
  3. 使用说明书:自行撰写一个word文档,命名为“学号_姓名_使用说明书。内容必须包含(a)一张有代表性的模型绘制截图,(b)模型的层次结构框图,(c)鼠标和键盘的具体交互用法, 即对如何使用鼠标和键盘与虚拟场景模型进行交互描述清晰并配有一定的截图说明。
  4. 上传格式:按上述要求完成实验,一并提交电子版过程说明报告、使用说明书和源代码压缩包。文档程序压缩包名称为“学号_姓名_期末大作业”。
  5. 截止时间
  6. 答辩说明:第18周理论课和实验课将安排课程大作业答辩,按照学号顺序,每人上台展示自己的期末大作业。具体答辩安排等候后期通知。

学生作业:

一、整体游戏设计

游戏描述:

“操纵角色”站在某个“方块”上,然后在该“方块”的 前后左右 的某个方向,在一定距离生成一个 新的“方块”。点击鼠标使“操纵角色”跳跃至新的方块上,跳到则加分,跳不到则游戏结束。其中,按住鼠标左键能调节跳跃力度,放开鼠标左键则开始跳跃。

注:本实验“跳一跳”游戏 参考自 微信“跳一跳”小程序游戏。

 

设计图解:

图 1 :“跳一跳”设计图解

 

说明:

① 新生成方块的前后左右 生成方向是随机的,但是不会在上一个方块的位置方向上生成。

② 方块间的距离在一定范围内也是随机的,确保“操纵角色”的最大跳跃距离大于它。

③ 生成的方块为立方体,尺寸相等。

 

二、图形的基本绘制及相关说明

1、obj模型的绘制

1)获取obj文件的UV坐标数据,顶点坐标数据,顶点法线数据,存储三角面片的顶点索引数据(包括顶点坐标、UV坐标、顶点法线索引)。

读取obj文件注意一下几点即可:

① 把顶点数据存储到m_vertices_(按每个顶点的x、y、z坐标依次存储);

     把法线数据存储到m_normals_(按每条法线的x、y、z坐标依次存储);

     把顶点颜色数据存储到m_color_list_(由顶点法线决定。按每个顶点颜色的r、g、b坐标依次存储);

     把纹理坐标数据存储到m_vt_list_(按每个纹理坐标的x、y坐标依次存储);

     把索引信息存储到m_faces_(以每个面的三个顶点为一组数据,可知一组数据有9个值,这9个数据值依次按:3个顶点坐标点索引、3个uv坐标点索引、3个法线坐标索引 顺序存储)。

 

因为obj文件索引是从1开始的,为方便找顶点相关数据,先给相关容器加一个0。

 

③ 用getline(fin, str)函数逐行检测(fin为obj文件的ifstream流,str为读取的当行文本字符串)。每读一行检测第一个空格前的字符串是否为“v”“vt”“vn”或“f”。

     是,则要把相关数据用>>按空格分开读取后压入到相关容器中。读取索引数据时,每读一个顶点的三个索引数据(即:顶点索引/uv点索引/     法线索引),要用getline函数按'/'作为分隔符分割;

 

2)由索引确定顶点相关信息并传给shader

在update_vertex_buffer函数申请顶点数组对象和顶点缓存对象,获得足够的空间存储坐标,颜色,法线以及纹理坐标。

已知绘制各个面所需的数据索引,因此只要根据索引值到数据容器中寻找相应的值即可。

以顶点坐标数据为例:

Mesh_Painter.cpp文件中获取到我们刚刚获得的无序顶点坐标数据容器,赋给新容器vertex_list;同理获得存储索引数据的新容器face_list;num_face为面的个数,易知值为face_list.size() / 9 。

现在,我们想通过face_list中的顶点坐标索引值获取有序的顶点坐标、并存储在容器points中。 已知face_list每9个数据为1组,且顶点坐标索引在前3个数据;而vertex_list则是以每3个数据为1组。

易知其获取方法,以下为核心代码:

//////////////////////////////////////////////////////////////////////

for (int i = 0; i < num_face; i++)

{

int index = face_list[9 * i];

points[3 * i] = vec3(

(vertex_list[index * 3 + 0] - p_center_.x) / (1.5 * d),

(vertex_list[index * 3 + 1] - p_center_.y) / (1.5 * d),

(vertex_list[index * 3 + 2] - p_center_.z) / (1.5 * d)

);

index = face_list[9 * i + 1];

points[3 * i + 1] = vec3(

(vertex_list[index * 3 + 0] - p_center_.x) / (1.5 * d),

(vertex_list[index * 3 + 1] - p_center_.y) / (1.5 * d),

(vertex_list[index * 3 + 2] - p_center_.z) / (1.5 * d)

);

index = face_list[9 * i + 2];

points[3 * i + 2] = vec3(

(vertex_list[index * 3 + 0] - p_center_.x) / (1.5 * d),

(vertex_list[index * 3 + 1] - p_center_.y) / (1.5 * d),

(vertex_list[index * 3 + 2] - p_center_.z) / (1.5 * d)

);

}

GLintptr offset = 0;

glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(vec3)*num_face * 3, points);

//顶点坐标传给shader

offset += sizeof(vec3)*num_face * 3;

delete[] points;

//////////////////////////////////////////////////////////////////////

其余顶点相关信息获取方法类似,不细说明。

3)根据模型的各个信息进行图形绘制,同时要对纹理贴图进行创建和赋值,这里调用外部类FreeImage来实现。

 

2、矩形图形的绘制

   绘制流程与上述obj图形的绘制流程比较相像,唯一不同的是obj文件的所有信息是通过读取文件得到的,而矩形图形的所有信息都是自己写的,参考以下图形坐标点及绘制顺序,和纹理坐标点来进行点信息的写入:

图 2:矩形图形顶点坐标点及顶点的建立顺序图

 

图 3:矩形图形纹理坐标图

 

 

3、模视变换和透视投影

 1)模型变换矩阵为单位矩阵

 2)相机观察矩阵根据eye(视点)、at(参考点)、up(向上变量)三参数确定。

核心代码:

mat4 lookAt(const vec4& eye, const vec4& at, const vec4& up)

{

//归一化:normalize

//向量积:cross

vec4 n = normalize(at - eye);

vec4 u = normalize(vec4(cross(n, up), 0.0));

vec4 v = normalize(vec4(cross(u, n), 0.0));

vec4 t = vec4(0.0, 0.0, 0.0, 1.0);

mat4 c = mat4(u, v, -n, t);

return c * Translate(-eye);

}

3)本实验采用透视投影,透视投影变换矩阵由fov(视角)、aspect(投影平面长宽比)、zN(近裁剪平面)、zF(远裁剪平面) 四个参数决定的。

核心代码:

mat4 perspective(const GLfloat fovy, const GLfloat aspect,

const GLfloat zNear, const GLfloat zFar)

{

GLfloat top = tan(fovy*DegreesToRadians / 2) * zNear;

GLfloat right = top * aspect;

 

mat4 c;

c[0][0] = zNear / right;

c[1][1] = zNear / top;

c[2][2] = -(zFar + zNear) / (zFar - zNear);

c[2][3] = (-2.0*zFar*zNear) / (zFar - zNear);

c[3][2] = -1.0;

c[3][3] = 0.0;

return c;

}

 

4)本实验是在上述相机坐标系下进行的,而且所有模型都乘上了该透视投影矩阵。

 

4、阴影的绘制及光照模型

1)阴影变换矩阵为:

根据光源坐标lx,ly,lz值,可求得投影在y=0平面的阴影变换矩阵为:

mat4 shadowProjMatrix = mat4(-ly, 0.0, 0.0, 0.0,

lx, 0.0, lz, 1.0,

0.0, 0.0, -ly, 0.0,

0.0, 0.0, 0.0, -ly);

 

由于在本实验中,并不是所有物体都需要进行阴影绘制,所以要用一个变量shadowflag去判断某个模型是否需要进行阴影绘制。

而绘制阴影需要用到黑色,所以绘制阴影时给片元着色器传入一个flag来判断要绘制的是带纹理的模型还是阴影,确定绘制颜色fcolor。

 

2)本实验依旧采用phong光照反射模型,根据模型的顶点坐标及法向量,和光源坐标,设定好环境光反射、变量漫反射和镜面反射变量,即可确定反射光照。

相关核心系数及计算见片元着色器文件代码:

void main()

{

vec3 ambiColor = vec3(0.3, 0.3, 0.3);

vec3 diffColor = vec3(0.7, 0.7, 0.7);

vec3 specColor = vec3(0.4, 0.4, 0.4);

 

// TODO 计算N,L,V,R四个向量并归一化

vec3 N_norm = normalize(N);

vec3 L_norm = normalize(lightPosition - V);

vec3 V_norm = normalize(-V);

vec3 R_norm = reflect(-L_norm, N_norm);

 

// TODO 计算漫反射系数和镜面反射系数

float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);

float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);

if(flagcolor==0)

fColor = vec4(0.0, 0.0, 0.0, 1.0);

else

{

float shininess = 10;

fColor = vec4(ambiColor +

diffColor * lambertian +

specColor * pow(specular, shininess), 1.0) * texture2D( texture, texCoord );

}

    fNormal = normal;

}

 

5、平移、缩放、旋转变换

平移、缩放和旋转功能就是把它们对应的变换矩阵乘上当前作用点来实现。

其中,平移、缩放、绕xyz轴旋转的变换矩阵可以直接调用函数Translate()、Scale()、RotateX()、RotateY()、RotateZ() 生成返回值。

①平移:

  函数:Translate(vec3(tx,ty,tz))

②缩放:

  函数:Scale(vec3(Sx,Sy,Sz))

绕x轴旋转:

  函数:RotateX(theta)

④绕y轴旋转:

  函数:RotateY(theta)

⑤绕z轴旋转:

  函数:RotateZ(theta)

 

 

三、初始场景的搭建

1、背景颜色

背景颜色绘制为天蓝色,代码为:glClearColor( 0.5, 0.6, 1.0, 1.0 );

 

2、场景层次结构设计

图 4:场景层次结构设计图

 

由上图可见场景图层设置大概有五层,分别为:

①  第一层(y=-0.2):地面层

            该层地面主要由上文提到的矩形模型实现。绘制矩形图形后并进行缩放平移旋转等调整,给其赋上纹理贴图即可。

            而该层纹理贴图有四种,可根据随机函数随机选择一种贴图进行加载。

            该图层不需要绘制阴影,shadowflag判断变量要置为0 。

            部分代码如下:

        //选择贴图

        int floorimg = rand() % 4;

        if (floorimg == 0)

       str = "texture/floor.jpg";

        else if(floorimg == 1)

       str = "texture/tudi.jpg";

        else if (floorimg == 2)

       str = "texture/caodi.jpg";

        else

       str = "texture/shamo.jpg";

 

        My_Mesh* my_mesh9 = new My_Mesh;

        my_mesh9->generate_square();

        my_mesh9->set_texture_file(str);

        my_mesh9->set_translate(-1.0, -0.2, -1.5);

        my_mesh9->set_scale(2.5, 2.5, 2.5);

        my_mesh9->set_theta(180, 0, 0.);

        my_mesh9->set_theta_step(0, 0, 0);

        my_mesh9->set_shadowflag(0);

        my_meshs.push_back(my_mesh9);

    mp_->add_mesh(my_mesh9);

 

②  第二层(y=-0.1):游戏分数展示层

            该层地面主要由上文提到的矩形模型实现。绘制矩形图形后并进行缩放平移旋转等调整,给其赋上带有具体数字的纹理贴图即可通过贴图的方式展示分数。

③  第三层(y=0):阴影层

            展示模型阴影。

 

④  第四层(y>0):方块层

            在y=0平面上绘制方块,初始时加载obj模型绘制三个方块。其中,方块的纹理贴图有26种选择,贴图的选择同样为用随机函数随机选择。

            其中,方块具有比较重要的属性有renewflag,该变量表示该方块的编号。编号不同表示的意义为:

            renewflag=1:表示当前角色所在的方块。

            renewflag=0:角色上一个所在的方块。

            renewflag=2:角色即将要跳至的下一个方块。

            为突出跳跃目标,给renewflag=1的方块设置一个旋转变量的增量,使其每次绘制时旋转角度都加上这一增量,从而模拟出方块在自动旋转的效果。

 

            方块比较重要的属性还有direction,该变量能表示下一个方块出现的方向。如下图方块,编号表示的下一个方块出现的方向:

            

图 5:下一个方块出现的方向及对应编号

 

⑤  第五层(y>0.5):角色层

            由于方块缩放后的边长为0.5,因为角色要站在方块上,所以角色层在y=0.5的平面上方。本次实验使用之前实验提供的wawa.obj来模型作为“操纵角色”。

            角色比较重要的属性有renewflag,其含义与上述方块的renewflag属性含义相同,该属性在角色中可以描述为“角色即将跳跃的方向”。

            同时,角色还有一个face属性,表示当前角色面的朝向,具体方向编号与renewflag相同。

 

由于本项目为一个小游戏,并且许多过程都需要重载读取模型或截图信息,所以不适宜加入太多其余与游戏无关的模型,所以场景比较单调,但是这是为游戏流畅性所必需的。

 

运行程序,初始化为(由于贴图选择具有随机性,下列展示其中三种初始化情况):

    

图 6:初始图1

 

   

图 7:初始图2

 

   

图 8:初始图3

 

 

四、键盘控制视角切换

本实验主要是通过键盘按键调用函数改变eye的相关参数来实现视角切换的,而这种改变eye值的视角切换会使镜头围绕at参考点进行变化。

核心代码:

void Mesh_Painter::update_eye()

{

float xx = rad * cos(tAngle * DegreesToRadians) * sin(pAngle * DegreesToRadians);

float yy = rad * sin(tAngle * DegreesToRadians);

float zz = rad * cos(tAngle * DegreesToRadians) * cos(pAngle * DegreesToRadians);

eye = vec4(xx, yy, zz, 1.0);

}

由上代码可见决定eye值参数有rad、tAngle、pAngle,键盘按键调用函数改变这三个变量值即可。

以下相机镜头的变换都是围绕参考点进行的:

键盘控制表如下:

W/w:相机绕着参考点往上移(增加tAngle)

S/s:相机绕着参考点往下移(减小tAngle)

A/a:相机绕着参考点往左移(减小tAngle)

D/d:相机绕着参考点往右移(增加pAngle)

Q/q:拉远相机(增加rad)

E/e:拉近相机(减小rad)

Space:重开游戏

Esc:退出程序

 

如调整镜头后:

图 9:调整镜头1

 

图 10:调整镜头2

 

五、鼠标左键按下后控制角色弹跳力度

由代码if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) 可令其在按下鼠标左键时,触发后面的事件,这里触发的事件是使程序绑定一个空闲回调函数的对象idle2。

该函数能不断调用当前角色所在方块的函数add_scale_step(),该函数能减小Scale缩放函数的y参数,使方块在y轴方向发生缩小变化,同时角色也会跟随着每次变化,也变化相应的位移。从而模拟角色正在像弹簧般压缩方块的动作。

注意当压到一定的限度时则不能再往下压了。

核心代码为:

void

idle2(void)

{

bool flag;

/*要先判断三个方块哪个方块是当前角色所在方块,

该方块需要y轴上的缩小,模拟类似弹簧被压缩的过程*/

if (my_meshs[1]->get_renewflag() == 1)

flag = my_meshs[1]->add_scale_step(-0.0004);

else if (my_meshs[2]->get_renewflag() == 1)

flag = my_meshs[2]->add_scale_step(-0.0004);

else if (my_meshs[3]->get_renewflag() == 1)

flag = my_meshs[3]->add_scale_step(-0.0004);

 

//每次方块成功压缩一点时,角色也往下位移同等距离

if(flag)

my_meshs[0]->add_trans_step(0, -0.0004, 0);

 

glutPostRedisplay();

}

 

//mesh.cpp

//改变y轴方向的缩放

bool My_Mesh::add_scale_step(float step)

{

if (Scale[1] > 0.2)

{

Scale[1] += step;

return true;

}

return false;

}

//改变平移

void My_Mesh::add_trans_step(float x, float y, float z)

{

vTranslation[0] += x;

vTranslation[1] += y;

vTranslation[2] += z;

}

 

如下图所示压缩到最大后的情景:

图 11:方块压缩图

 

六、鼠标左键放开,角色和方块快速复原到压缩前情景

上述如果一直按住鼠标没放开,程序就会一直空闲调用idle2函数,模拟压缩过程,并且最后压缩到一定地步时不会再变化。

而此时如果松开鼠标左键则会因为代码else if (button == GLUT_LEFT_BUTTON && state == GLUT_UP)判断鼠标松开,从而执行别的事件,这里会把空闲回调函数对象切换至idle3,这样方块压缩过程就结束了。但在这之前,还要先获取方块y轴上的缩小参数,以确定压缩率,才能得知待会角色要跳多远。

调用idle3函数后能让压缩方块和角色很快的恢复压缩前状态(该恢复过程也是动态的)后,立即切换绑定空闲回调函数对象到idle4,基本无缝接入待会角色的弹跳过程。

 

该代码比较简单,原理与上述第五部分大致相同,因此不一一描述。

 

七、压缩方块回弹 及 角色弹跳

该过程比较复杂,主要内容包括:

  • 根据上一部分得到的压缩过程中得到的压缩方块的y轴上的缩小参数(范围为0.2 ~ 0.5),可确定压缩率为:

       float multiply = (0.5 - yy) / (0.5 - 0.199);   //压缩率: 0 < multiply < 1

 

  • 根据压缩率,设计算法控制跳跃时长(或者说控制跳跃帧),跳得越远,时间应该越长。

 

  • 根据跳跃方向选择相对应的位移变化,调用改变位移参数的函数即可,位移高度与长度都与压缩率成正比关系。

 

  • 如果压缩率达到了0.7,角色要有一个翻滚动作。翻滚动作实则为改变旋转矩阵参数即可改变,而角色翻滚动作的参考轴 共同取决于 角色的面朝向(face)和要跳的方向(direction)。

 

  • 模拟角色刚弹出时,原压缩方块要有一定程度的回弹,实则为y轴上小幅程度的缩放变化。该回弹缩放变化量也要与压缩率成正比关系。

 

  • 角色跳跃完成后,要进行检测,判断角色是否成功落到目标方块上(后文会详解该部分)。

 

根据以上内容设计代码如下:

////////////////////////////////////////////////////////////////////////////////////////

//角色弹出 及 压缩方块回弹

void

idle4(void)

{

int direction = my_meshs[0]->get_direction();

float d = 2.0;  //

float multiply = (0.5 - yy) / (0.5 - 0.199);   //压缩率: 0 < multiply < 1

 

//获取当前方块

int i;

if (my_meshs[1]->get_renewflag() == 1)

i = 1;

else if (my_meshs[2]->get_renewflag() == 1)

i = 2;

else if (my_meshs[3]->get_renewflag() == 1)

i = 3;

 

//控制跳跃时长(控制跳跃帧)。跳得越远,时间应该越长

int multime = 500;

float time_temp = 48.0 * multiply * multiply;

multime = time_temp + 50;

int face = my_meshs[0]->get_facedirect();

int direct = my_meshs[0]->get_direction();

 

int tt;

float dd = 1.2;

//初始位置到最高点过程

if (t >= 1 && t <= multime)

{

//拖慢程序让弹跳时,最高点的滞空时间长一点

tt = t*t*3;

while (tt--)

std::cout << "";

 

//根据跳跃方向选择相对应的位移变化,长度和高度位移都与压缩率成正比关系

if (direction == 0)

my_meshs[0]->add_trans_step(0.0, dd * multiply / multime, multiply * d / (2 * multime));

else if (direction == 1)

my_meshs[0]->add_trans_step(multiply * d / (2 * multime), dd * multiply / multime, 0);

else if (direction == 2)

my_meshs[0]->add_trans_step(multiply * d / (-2 * multime), dd *  multiply / multime, 0);

else if (direction == 3)

my_meshs[0]->add_trans_step(0, dd * multiply / multime, multiply * d / (-2 * multime));

 

my_meshs[i]->add_scale_step(-0.0007); //模拟角色刚弹出时,方块有一定程度的回弹

 

//如果压缩率达到了0.7,角色要有一个翻滚动作

if (multiply > 0.7)

{

//角色翻滚动作的参考轴 取决于 角色的面朝向和要跳的方向

if (face == 3 && direct == 3)

my_meshs[0]->add_theta(360.0 / (-2 * multime), 0.0, 0.0);

else if (face == 2 && direct == 2)

my_meshs[0]->add_theta(0.0, 360.0 / (-2 * multime), 0.0);

else if (face == 1 && direct == 1)

my_meshs[0]->add_theta(0.0, 360.0 / (2 * multime), 0.0);

else if (face == 0 && direct == 0)

my_meshs[0]->add_theta(360.0 / (2 * multime), 0.0, 0.0);

else if (face == 3 && direct == 1)

my_meshs[0]->add_theta(0.0, 360.0 / (2 * multime), 0.0);

else if (face == 3 && direct == 2)

my_meshs[0]->add_theta(0.0, 360.0 / (-2 * multime), 0.0);

else if (face == 2 && direct == 3)

my_meshs[0]->add_theta(360.0 / (-2 * multime), 0.0, 0.0);

else if (face == 1 && direct == 3)

my_meshs[0]->add_theta(360.0 / (-2 * multime), 0.0, 0.0);

else if (face == 2 && direct == 0)

my_meshs[0]->add_theta(360.0 / (2 * multime), 0.0, 0.0);

else if (face == 0 && direct == 2)

my_meshs[0]->add_theta(0.0, 360.0 / (-2 * multime), 0.0);

else if (face == 0 && direct == 1)

my_meshs[0]->add_theta(0.0, 360.0 / (2 * multime), 0.0);

else if (face == 1 && direct == 0)

my_meshs[0]->add_theta(360.0 / (2 * multime), 0.0, 0.0);

}

}

//最高点到跳跃终点的过程

else if (t >= (multime + 1) && t <= (2 * multime))

{

//拖慢程序让弹跳时最高点的滞空时间久一点

tt = (t - (multime + 1))*(t - (multime + 1))*3;

while (tt--)

std::cout << "";

 

//根据跳跃方向选择相对应的位移变化,长度和高度位移都与压缩率成正比关系

if (direction == 0)

my_meshs[0]->add_trans_step(0.0, dd * multiply / (-1 * multime), multiply * d / (2 * multime));

else if (direction == 1)

my_meshs[0]->add_trans_step(multiply * d / (2 * multime), dd * multiply / (-1 * multime), 0);

else if (direction == 2)

my_meshs[0]->add_trans_step(multiply * d / (-2 * multime), dd * multiply / (-1 * multime), 0);

else if (direction == 3)

my_meshs[0]->add_trans_step(0, dd * multiply / (-1 * multime), multiply * d / (-2 * multime));

 

my_meshs[i]->add_scale_step(0.0007); //模拟角色刚弹出时,方块有一定程度的回弹

 

//如果压缩率达到了0.7,角色要有一个翻滚动作

if (multiply > 0.7)

{

//角色翻滚动作的参考轴 取决于 角色的面朝向和要跳的方向

if (face == 3 && direct == 3)

my_meshs[0]->add_theta(360.0 / (-2 * multime), 0.0, 0.0);

else if (face == 2 && direct == 2)

my_meshs[0]->add_theta(0.0, 360.0 / (-2 * multime), 0.0);

else if (face == 1 && direct == 1)

my_meshs[0]->add_theta(0.0, 360.0 / (2 * multime), 0.0);

else if (face == 0 && direct == 0)

my_meshs[0]->add_theta(360.0 / (2 * multime), 0.0, 0.0);

else if (face == 3 && direct == 1)

my_meshs[0]->add_theta(0.0, 360.0 / (2 * multime), 0.0);

else if (face == 3 && direct == 2)

my_meshs[0]->add_theta(0.0, 360.0 / (-2 * multime), 0.0);

else if (face == 2 && direct == 3)

my_meshs[0]->add_theta(360.0 / (-2 * multime), 0.0, 0.0);

else if (face == 1 && direct == 3)

my_meshs[0]->add_theta(360.0 / (-2 * multime), 0.0, 0.0);

else if (face == 2 && direct == 0)

my_meshs[0]->add_theta(360.0 / (2 * multime), 0.0, 0.0);

else if (face == 0 && direct == 2)

my_meshs[0]->add_theta(0.0, 360.0 / (-2 * multime), 0.0);

else if (face == 0 && direct == 1)

my_meshs[0]->add_theta(0.0, 360.0 / (2 * multime), 0.0);

else if (face == 1 && direct == 0)

my_meshs[0]->add_theta(360.0 / (2 * multime), 0.0, 0.0);

}

}

//跳完

else

{

my_meshs[0]->retransY(); //细微调整角色到最准确的y方向位置

my_meshs[i]->rescale();  //细微调整压缩方块到最准确原大小

t = 1;

check_flag = mp_->check();  //跳跃完成后检测其是否正确跳到目标方块上

glutIdleFunc(idle5);   //切换空闲回调函数对象

}

t++;

glutPostRedisplay();

}

////////////////////////////////////////////////////////////////////////////////////////

 

八、落点检测

这一部分主要是通过获取角色和目标方块(或者原角色所在方块)的平移位移量xz来进行比较,保证角色的x和z坐标要在目标方块的一定范围内。比如,假设x、z为角色xz坐标,xx、zz为 原角色所在方块 或者 目标方块 的xz坐标,这里可以令:

if (x <= (xx + 0.31) && x >= (xx - 0.31) && z <= (zz + 0.31) && z >= (zz - 0.31))

判断角色是否在方块的一定范围内,根据结果返回值并把结果值赋给check_flag即可。

结果为0表示角色没跳在任何方块上;结果为1表示角色落在原方块上;结果为2表示角色落在新方块上。

 

原理简单,不展示代码。

 

 

九、角色和目标方块的回弹,以及角色转向

这一部分内容为:

  • 根据上一部分得到的检测结果,如果check_flag为1,则表示此时角色还是落在原来的方块上,则此时角色小幅度弹跳(平移。根据方块压缩率动态变化)。重新绑定回仅有重绘功能的空闲函数回调对象idle,等待下一次跳跃。
  • 如果check_flag为0,则表示角色没跳到任何方块上。此时平移掉落地上,并把gameflag设为0,表示game over,空闲回调函数的对象绑定到idle7,等待发落。
  • 如果check_flag为2,则表示角色成功跳到新方块上。此时要根据角色的face和direction属性确定要转向的角度(角色转向是为了 让角色面对的方向 能根据新方块出现的位置 而作出调整)。在角色跳落至新方块的一瞬间,模拟回弹过程,角色微微弹起(平移。平移量由压缩率决定),弹起上升过程中角色同时实行转向(旋转)。而角色回弹过程中,新方块也有回弹(y方向缩放)。完成该过程则把空闲回调函数的对象绑定到idle6。

 

部分代码:

//回弹和转向

if (t >= 1 && t <= 80 && check_flag != 0)

{

my_meshs[0]->add_trans_step(0, 0.005 * multiply, 0); //角色落在方块上,弹跳

if (check_flag != 1) //角色落在了新的方块上

{

my_meshs[0]->add_theta(0.0, 0.0, theta / 80); //弹跳上升过程中角色转向

my_meshs[i]->add_scale_step(0.0008); //新方块回弹

}

}

else if (t >= 81 && t <= 160 && check_flag != 0)

{

my_meshs[0]->add_trans_step(0, -0.005 * multiply, 0); //角色落在方块上,弹跳

if (check_flag != 1) //角色落在了新的方块上

my_meshs[i]->add_scale_step(-0.0008); //新方块回弹

}

 

else if (t >= 1 && t <= 50 && check_flag == 0)  //掉落地面

{

my_meshs[0]->add_trans_step(0, -0.5/50, 0);

}

 

十、旧方块下陷

当角色成功跳到目标方块后,回调函数会变为idle6,这一部分会使旧的方块往地下陷落。旧的方块就是指renewflag为0的方块(前问提过,当前方块renewflag为1;下一个跳跃目标方块为2,上一个旧方块则为0)。

这一部分就是普通的y轴向下逐步平移即可,无需细述。

值得注意的是,为了不把地下方块的阴影投到y=0平面上,要把shadowflag设为0,不绘制其阴影。

完成后空闲回调函数绑定为idle7。

 

十一、视点跟踪

第十部分以及第九部分的game over状态后,都会绑定idle7为空闲回调函数对象。这一部分会让相机动态平移,保持参考点仍为操纵角色,相机视角仍以角色为中心。

这一部分原理也比较简单,根据角色的平移方向和平移量即可让相机的eye值和at值做一样的变化了。

需要注意的是,由于“分数展示界面”要一直保持在相机镜头的固定位置,所以“分数展示界面”(实质为矩形模型)也要做同样的平移变化。

 

完成以上操作后,这一部分还要通过gameflag来判断是否已经game over了,如果game over了则给操纵角色设置一个旋转增量,让其旋转起来,为后面的游戏结束场景做准备。空闲回调函数绑定至idle9。

如果没有game over,则通过函数newblock()来创建一个新的方块(其实只是改变了旧方块的位置和贴图),创建成功后空闲回调函数对象绑定为idle8。

 

十二、“创建”新方块

该部分包括以下内容:

  • 给所有(三个)方块的renewflag编号减1,减至-1时则变为2,因为该编号的值只能为0,1,2 。
  • 编号减1后,给当前编号为1的方块停止自转,给当前编号为2的方块设置旋转增量。
  • 随机生成一个新方向,并保证不会在前一个方块的方向生成(即如果在前一个方块方向生成了则重新随机取数)。
  • 随机生成一个以当前所在方块为中点的距离。 因为游戏限定在一定区域内,如果生成方块位置超过了该区域,则要重新生成方向和距离。
  • 成功随机生成上述方向和距离数据后,把它作为平移参数赋给renewflag=2的方块(这里y坐标值要调整,要保证新方块暂时在地底下),此时便确定了新方块的位置。
  • 更新当前所在方块和角色的方向,这一方向即为即将新生成方块的方向。
  • 由于之前方块潜入地底不绘制阴影,此时要把shadowflag打开。
  • 由于是“新方块”,那么贴图就不能是之前旧的。所以此时要重新随机选取方块贴图(26种),选好后新的纹理贴图,并更新绘制。

 

    由上一部分可知,“创建”新方块这一部分函数完成后,空闲回调函数对象变为了idle8。

 

十三、新方块由地底升起 及 回弹

该idle8回调函数对象 对renewflag=2的方块执行y轴向上的平移操作,模拟新方块从地底冒出的动态过程。

新方块冒出来后,立马切换空闲回调函数为idle10,对其进行回弹操作(即y轴缩放,上文多次提到“回弹”,不再细述),这样可以增加新方块“冒土”后的动感。

操作完成后则重新把空闲回调函数的对象切换至idle,等待下轮操作。

 

十四、游戏结束阶段

在第十一部分“视点跟踪”部分判断gameflag是否为0,是0则表示game over,除了对角色模型设置旋转增量使其自转外,还立马把空闲回调函数对象切换至idle9函数。

这一部分的内容有:

  • 对角色模型进行向上的动态平移(保证会移出默认的相机视野外),此时场景就会变为:角色模型落在地上后,螺旋上升直到移出相机视野。
  • 三个方块都动态往地底下陷落(平移)。
  • 地面上会新浮现出两个矩形图形,分别是——贴图写着“Game Over”的矩形 以及 “press“space”restart”的矩形。    而这两个矩形是初始化过程中就已经创建好了的,只不过正常游戏时,它们都在地面下方,被地面掩盖住了看不到,而在游戏结束阶段则会平移上来。(相机视野变换过程中这两个矩形图形也会做一样的平移)
  • 以上角色模型和方块模型的shadowflag都设为0,不绘制阴影。

 

游戏结束例图如下:

   

图 12:game over图

 

十五、其他注意事项及说明

1、空格键重新开始游戏

        重新开启游戏其实就是把存储绘制图形的数组的信息都给清除,如my_meshs.clear(),把空闲回调函数对象设为初始的idle,同时把eye和at等相关数据值都设为初始值,再对模型进行初始读取载入绘制即可。

2、设置按下鼠标后的标志mouseflag和模型运动的标志moveflag。

        设置这些标志能有效保证程序游戏能够正常的运行,而不会出现一些奇怪的bug。比如,物体运行过程中,moveflag为1,此时按下鼠标左键,程序会判断moveflag,为1则表示在运动,无法执行按下鼠标后的操作,这样就保证程序能正常执行逻辑。  当模型运动完后moveflag就会设为0 。

        同理,为尽量避免bug也要设置mouseflag,比如我们已经知道当物体运动时按下鼠标左键是没有后续事件响应的,而此时如果物体运动完了松开鼠标左键,就会因为没进行“方块压缩”过程而发生后续操作,产生逻辑错误。   当按下鼠标左键mouseflag赋值为1,松开鼠标左键后要判断mouseflag是否为1才能执行后续操作,如果为1则执行后续操作并把mouseflag再设回为0 。

        空格重启游戏时也要对这些标志变量进行初始化。

3、得分设计。

得分score从0开始,每次成功跳到目标方块后,则增加scoremul。

而scoremul初始为1,每当角色模型落在离目标方块很近的地方时(距离差小于成功落点的距离差),则+1 。

比如说,该实验中,个人设定落点成功的判定范围见该语句:

if (x <= (xx + 0.31) && x >= (xx - 0.31) && z <= (zz + 0.31) && z >= (zz - 0.31)

而scoremul+1的距离判定范围见该语句:

if (x <= (xx + 0.05) && x >= (xx - 0.05) && z <= (zz + 0.05) && z >= (zz - 0.05))

 

而当落点成功却没落在该scoremul+1的指定范围内时,scoremul重置为1 。

 

程序每次算完得分后,就会在当前该函数内根据得分去重新选择读取载入相对应的数字纹理贴图以更新 分数展示界面。

 

猜你喜欢

转载自blog.csdn.net/purers/article/details/82625110
今日推荐