1、华中科技大学电子科学与技术系课程设计报告( 2010- 2011 年度第 2 学期)名 称: 软件课程设计 题 目: 基于 OpenGL 的 3D 旋转魔方实现 院 系: 班 级: 学 号: 学生姓名: 指导教师: 设计周数: 成 绩: 日期: 年 月 日1目 录1.课程设计介绍 21.1 目的 21.2 内容 21.3 取得的成果22.程序分析 32.1 程序原理32.2 程序流程42.3 数据结构132.4 重要函数133.程序分析与结果演示163.1 成果演示163.2 程序分析174.出现过的问题1825.心得和小节1931.课程设计介绍1.1 目的21 世纪是高科技时代,是信息技术时
2、代,而计算机技术无疑会引领各行各业,为我们带来一个全新的时代。作为新世纪的接班人,我们必须拥有良好的计算机应用能力,才能跟上世界发展的大流,不至于在激烈的竞争中被淘汰。而程序作为计算机的灵魂,因此编程能力对当代大学生来说至关重要。通过本课程单元的学习,可以对软件工程项目从整体上有一个较清晰的了解和认识;可以提高自身软件编程能力,培养对计算机编程兴趣,培养良好的编程习惯。同时编程时的态度和方法对我们今后的学习和工作也有重要影响。所以整体看来软件课程设计这门课程提高了我们计算机使用水平,培养了我们良好的学习态度,对我们个人的发展而言有着重要的意义。1.2 内容(1)巩固和加强 c 语言相关编程知识
3、,学会用 Visual C+6.0 进行 c 语言编程。(2)掌握程序设计流程和思想,模块化结构分析以及程序设计流程,初步培养需求分析、软件测试、调试的能力。(3)掌握 win32 相关编程知识,了解 windows 程序内部运行机制。(4)掌握 OpenGL 贴图技术原理与函数实现,掌握 OpenGL 几何的移动、旋转等模式变化的原理。(5)掌握魔方图形构造原理,在掌握二阶魔方构造原理的基础上,构造出三阶魔方并实现其旋转。1.3 取得的成果在理解和掌握老师所给的范例程序的基础上,借助 Win32 平台进行了一系列调试和学习,熟练掌握了 Win32 Application 开发流程。同时也学习
4、和了解了 OpenGL 的基本知识,掌握了一些 OpenGL 的重要技术与重要函数的使用,编写了一些简单的 OpenGL 程序。在比较透彻的了解了二阶魔方的构造原理后,成功地构造出了三阶魔方,换上了自己班级同学的图片,并4且在一个小立方体的六个面上贴上了不同的图片。能够比较完美的实现三阶魔方各个层面的随机旋转,并且把窗口背景设置为红色。为了使程序更加有趣,我在程序中导入了刘德华的爱你一万年这首歌,使魔方在旋转的同时能够播放歌曲。除此之外,我还实现了一种三阶魔方自由移动的屏保效果:即三阶魔方在旋转的同时能够在屏幕内部自由移动,并且在边缘无限次的反弹。在魔方平移的过程中同样可以通过四个方向键来控制
5、魔方的移动。当松开方向键后,魔方会继续按照先前的方式自由移动。2.程序分析2.1 程序原理(1)OpenGLOpenGL 是为 Open Graphics Library 的简称,它是 3D 绘图工业标准,广泛地应用于计算机 3D 绘图领域。它是个专业的开放的 3D 程序接口,是一个功能强大,调用方便的底层 3D 图形库。它独立于窗口系统和操作系统,以它为基础开发的应用程序可以十分方便地在各种平台间移植;OpenGL 可以与Visual C+紧密接口,便于实现机械手的有关计算和图形算法,可保证算法的正确性和可靠性;它具有七大功能:建模、变换、颜色模式设置、光照和材质设置、纹理映射、位图显示和图
6、象增强和双缓存动画功能。OpenGL 使用简便,效率高。本项目是在 Visual C+6.0 开发环境下,使用 OpenGL 函数库,绘制魔方并实现魔方贴图、随机旋转、以及键盘控制等功能。采用基本图形的绘图函数及定位函数,添加相应纹理来实现魔方模型的绘制。通过读取载入 BMP 文件,应用纹理贴图技术来完成对魔方旋转面的处理。通过 OpenGL 中对图形的旋转和平移函数来实现对魔方整体的旋转和平移。(2)旋转在建立好空间三维模型后,要实现魔方体每一层面的旋转。而魔方体每一层面的旋转归结于每一个小立方体的旋转。每个小立方体的旋转又最终归结于每个点的旋转。对于一个坐标为(x,y,z)的点,如果围绕
7、z 轴逆时针旋转角度为 a,则旋转之后 z 坐标不变,x 和 y 坐标分别变为 x*cosa - 5y*sina,x*sina + y*cosa,如图 1 所示:图 1这样,实现了每个点的旋转,针对每个立方体只需采用循环对 8 个点均采取旋转操作就可实现一个立方体的旋转。 (3)消息循环与定时器由于程序在运行时 CPU 只能执行一个任务,然而此项目中魔方在旋转的同时要实现平移,所以需要用到 Win32 中的定时器功能。此程序中要用到的定时器的函数原型为:SetTimer(HWND hWnd ,UINT nIDEvent,UINT uElapse,TIMERPROC lpTimerFunc)HW
8、ND hWnd 为窗口句柄,使程序和定时器建立联系,UINT nIDEvent 是定时器 ID,用于区分不同的定时器;UINT uElapse 为定时器触发周期,意味着多长时间执行一次;,TIMERPROC lpTimerFunc 为该定时器执行时触发的函数。所以控制好不同定时器的触发周期和触发函数,就能使魔方的各个层面的旋转和平移互不冲突。62.2 程序流程(1)WinMain 主函数WinMain 主函数是所有 Win32 程序的入口点。在 WinMain 函数里窗体的建立和消息循环,在消息循环中实现键盘、鼠标输入事件处理响应。在本程序中,要创建 Window 窗体和构建 OpenGL 设
9、备绘图环境。Window 窗体创建步骤: 窗体类注册:RegisterClass 设置显示分辨率:ChangeDisplaySettings 设置窗体大小:AdjustWindowRectEx 创建窗体:CreateWindowExOpenGL 绘图环境搭建: 获取设备绘图环境(DC,DeviceContext) :hDC=GetDC(hWnd) 选择绘图环境像素格式:ChoosePixelFormat (hDC, /定义一个点的 x,y,z 坐标值stPoint;typedef struct stPoint CubePoint8;/定义一个小立方体的 8 个顶点stCube;如图 2:70
10、( - 1 . 0 f , - 1 . 0 f , 1 . 0 f ) 1 ( 1 . 0 f , - 1 . 0 f , 1 . 0 f )2 ( 1 . 0 f , 1 . 0 f , 1 . 0 f )3 ( - 1 . 0 f , 1 . 0 f , 1 . 0 f )4 ( - 1 . 0 f , - 1 . 0 f , - 1 . 0 f )7 ( 1 . 0 f , - 1 . 0 f , - 1 . 0 f )6 ( 1 . 0 f , 1 . 0 f , - 1 . 0 f )5 ( - 1 . 0 f , 1 . 0 f , - 1 . 0 f )ZYYO图 2 stCub
11、e Cube27; /定义魔方体的 27 个小立方体其中一个难点是怎样根据各个点的坐标值构造出魔方体,其实只要定义好每个顶点的坐标就行了。但是三阶魔方必须定义 27*8=216 个顶点的坐标值,而且很难用 for 循环实现,因为各个顶点的 x,y,z 坐标值几乎没有什么规律。但是如果能够将一个小魔方体作为一个整体来看待,工作量似乎会减轻很多。先在整个魔方体中间定义一个基准小立方体,则整个魔方体的各个小立方体均可以通过这个基准立方体的平移来实现,各个小立方体上各点的平移向量和小立方体中心的平移向量相同。static stPoint CubePoint8= /定义好基准小立方体,边长为 1 -0.
12、5f, -0.5f, 0.5f, /0 - 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, /50.5f, 0.5f, -0.5f, /6 -0.5f, -0.5f, -0.5f, /7; 8基准小立方体平移得到一个小立方体 Cube0:for(int i=0;i /提供与多媒体有关的接口#pragma comment(lib, “WINMM.LIB“) /导入 winmm.lib 库, 实现对多媒体编程的支持在所建工程的文
13、件夹中新建名为 sound 的文件夹,并在其中放入 wav 格式的音乐爱你一万年 。编写函数 loadsound(),其调用函数 PlaySound(“sound爱你一万年.wav“,NULL,SND_LOOP|SND_ASYNC|SND_FILENAME)加载爱你一万年背景音乐。(7)定时器的调用我认为本程序中使用 Win32 系统的定时器很关键,因为程序只能一次执行一个线程,而魔方在自身旋转的同时还有各个层面的自由随机旋转,还加上自身的平移等,所以必须用到定时器。在本程序中我用到了 3 个定时器::SetTimer(hWnd,1,2,NULL),:SetTimer(hWnd,2,1,Tim
14、erProc),:SetTimer(hWnd,3,0.5,CubeWalk)。定时器:SetTimer(hWnd,1,2,NULL)的作用是把把其产生的消息加入消息队列中,由 WM_TIMER 接收并处理,以实现立方体的旋转。定时器:SetTimer(hWnd,2,1,TimerProc)的回调函数是 TimerProc。函数TimerProc 用来产生随机数。要控制各个层面的随机旋转,一共有 18 种情13况,包括每个方向上每个层面上的正转和反转。故在 TimerProc 函数中对随机数的情况进行判断:int r = rand(); if( r%18=0)enable_XM_roatate(
15、1);以上是 18 中情况中的一种,使魔方的 XM 层能够旋转,即在enable_XM_roatate(1)中进行如下赋值:rotAngle = 1 rotCount = 0rotX = 1rotDirect =1在 WM_TIMER 中判断 rotX,rotY ,rotZ 的值并进行相应的旋转操作。本程序中的另外一个定时器:SetTimer(hWnd,3,0.5,CubeWalk)作用是控制魔方的平移。前面已经介绍了魔方自身的平移函数 glTranslatef。在程序的最开始定义变量 RX 和 RY,glTranslatef(RX,0.0f,0.0f);glTranslatef(0.0f,R
16、Y,0.0f);则 RX 和 RY 代表魔方中心的横纵坐标。在回调函数 CubeWalk 中对 RX和 RY 值进行处理和改变就可以控制魔方的平移了。定时器的使用及其控制魔方的旋转和平移流程图如图 5:图 5 定时器使用流程图142.3 数据结构分析在本程序中为了很好的表示一个魔方体并且便于更好地计算,采用结构体和数组层层嵌套的形式。定义一个点的结构体,由 3 个坐标值构成: typedef struct GLfloat p3; stPoint;定义一个立方体的结构体,由 8 个顶点构成:typedef struct stPoint CubePoint8; stCube;定义由 27 个小立方
17、体构成的数组:stCube Cube27; 2.4 重要函数分析(1)窗口创建程序中的函数 GLvoid resizeScene(GLsizei width,GLsizei height )目的是重置 OpenGL 窗口的大小,具体又包含以下几个函数:glViewport(0,0,width,height)是 OpenGL 中的视口变化函数,作用是把视景体截取的图像按照怎样的高和宽显示到屏幕上。glMatrixMode():指定哪一个矩阵是当前矩阵。glMatrixMode 设置当前矩阵模式:GL_MODEVIEW,对模型视景矩阵堆栈应用随后的矩阵操作。GL_PROJECTION,对投影矩阵应
18、用随后的矩阵操作。glLoadIdentity()将矩阵清为单位矩阵,避免受其它矩阵操作的干扰,作用是将当前的用户坐标系的原点移到屏幕中心。gluPerspective 的作用是设置透视投影矩阵,即越远的东西看起来越小的效果。gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f)1545.0f 表示将视角设置为 45 度,(GLfloat)width/(GLfloat)height表示窗口的纵横比,0.1f 表示沿 z 轴方向的两裁面之间的距离的近处为0.1,100f 表示沿 z 轴方向的两裁面之间的距离的远处为 100
19、,即图像在z 轴方向上的坐标必须介于-0.1 到-100 之间,否则无法显示出来。(2)初始化在 InitGL(GLvoid)中对窗体进行初始化效果:glShadeModel(GL_SMOOTH)作用是启用阴影平滑。glClearColor(0.5f, 0.0f, 0.0f, 0.5f)作用是设置背景为红色。glClearDepth(1.0f),作用是设置深度缓存。glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST),作用是做精细的透视修正。loadsound(),作用是导入歌曲爱你一万年。(3)OpenGL 贴图glGenTextures(1, p
20、c1-CubePointi.p1 = pc2-CubePointj.p1;pc1-CubePointi.p2 = pc2-CubePointj.p2;(6)屏保函数控制魔方移动是通过定时器 Timer3 来实现的,而 Timer3 的回调函数为 CubeWalk()。在该函数中,只要控制 RX 和 RY 的增减即可。RX 和RY 为全局变量,初值为零,所以魔方最初的位置是在屏幕的中心。RX+=0.1RY+=0.1就可以控制魔方每次向斜方向运动。为了实现魔方的反弹效果,需要另外定义两个全局变量 turn_x 和 turn_y,初值分别为 1。且令RX+=(turn_x*0.01)RY+=(tur
21、n_y*0.01)当魔方达到边界时,将 turn_x 和 turn_y 中的一个值取相反值,就可以使 RX 或 RY 反向增长,从而达到反弹的效果。经过试验,确定屏幕横向宽大致为 16,纵向宽大致为 8。而判断魔方达到边界时不能用“=”来进行判断,因为会有误差。为了消除误差,采用 fabs()函数来进行一定范围内的判断。if(fabs(RX+8)0.01|fabs(RX-8)0.01)else if(fabs(RY+4)0.01|fabs(RY-4)0.01)173.程序分析与结果演示3.1 成果展示3.2 程序分析程序整体流程图分析如下:18图 6 程序整体流程图4.出现过的问题其实编程不可
22、能一遍成功,我在编程的过程中也遇到了很多问题。(1)我开始将范例程序看懂之后,一气呵成将三阶魔方的程序编写出来了,但是写出来的程序只能控制三阶魔方的一部分层面旋转。于是我想实现三阶魔方任一层面随机旋转的效果,但是程序写完时候运行结果却出人意料之外。可以明显的看到魔方各个层面的旋转发生冲突,导致魔方产生扭曲。于是我仔细分析产生这种的原因,想了很久,后来想到会不会是写程序时控制各个层面的旋转的控制量的值有重复,从而产生冲突。然后我就把一个方向上的三个层面旋转的控制量的值改成不同的 1,2,3,结果就可以正常运行了。19(2)我在实现三阶魔方效果后,就想实现三阶魔方的自由反弹效果,我构思的算法自认为
23、是对的,其实也比较简单,当时控制其反弹的源码如下:if(RX=-8|RX=8)turn_x=(-turn_x);else if(RY=-4|RY=4)turn_y=(-turn_y);RX+=(turn_x*0.01);RY+=(turn_y*0.01);但是实际运行效果就是与我预想的不一样。魔方到了边界,总是不反弹,径直的跑到屏幕外面去,不见了踪影。这个问题困扰了我很久,一直得不到解决。我曾经想过是不是与屏幕长宽表示错了有关,但是无论我把屏幕长宽设为多少,魔方总是不反弹。后来我偶然想到,会不会是因为定时器的执行周期太短,一次移动距离太短,产生了误差,所以到达边界时程序没有判断出来。于是我用了
24、 fabs 函数,给 RX 和 RY 设定了一定的误差范围,程序竟然可以运行成功了。这说明我的假设是对的。所以以后碰到问题要认真思考,也许灵感就在一瞬间闪现。5 心得和小节这段时间以来觉得自己收获还是挺大的。开始选这个课题是觉得三阶旋转魔方肯定比较有意思,看着也比较有挑战性。经过这段时间以来的学习,我加深了对三阶旋转魔方实现的认识,觉得这个课题的确是在我们的能力范围内,除了 OpenGL 的绘图知识比较陌生以外,其他的比如魔方的建模、控制魔方的旋转以及定时器的使用等函数或算法都是我们能力所能及的,当然这必须建立在有比较好的 c 语言基础上,能对 c 语言的数组、函数、指针及结构体有比较好的掌握
25、。其实编程是一个考验人耐心的事,有时候看着一个大型的程序,真有点无奈的感觉。当我对杨老师给的范例程序进行分析时,开始确实被吓着了,代码有20一千多行。而里面又有很多函数,也不知道哪些是库函数,哪些是自定义的函数。但是万变不离其宗,程序再怎么复杂,也总会遵循一定的规律,也会最终被分析透彻。于是我开始逐行逐行的分析范例程序。遇到不懂的代码我就上网查资料,看老师给的指导书,然后再旁边加上我理解的注释。大概花了两三个半天的时间,就基本上把范例程序全部看懂了。全部看懂之后的那个晚上,我躺在床上就一直在想二阶旋转魔方的实现过程,并考虑如何用类似的算法实现三阶旋转魔方。第二天上午打开电脑,趁着思路比较清晰,
26、我就开始在范例程序的基础上写三阶旋转魔方的程序,改了一些我认为应该修改的地方。这个过程还不是那么简单的,特别是构建魔方、计算魔方体顶点坐标时需要特别细致。修改的过程花了我一个多小时的时间。在修改完之后,我点了运行按钮,本来以为会有一大串 error 的,没想到竟然一次运行成功了,并且能够显示旋转的三阶魔方。当时真是欣喜若狂。因为这在我编程的经历中是很少出现的,说明了细致对编程的重要性。有时编程会出现意想不到的错误,甚至都不知道错误在什么地方,也许就是那个地方的分号没写,就会有几个 error。这时候就是考验人的细心和耐心的时候了。要沉得住气去找错误,解决错误,吸取教训。我想这种品质对我们今后的
27、学习和工作有着重要的意义。没有十全十美的程序,也没有十全十美的人,只有不断学习,不断改进,才能不断完善。在此感谢杨老师和帮助过我的同学,有了你们的帮助和指导,我在有些地方也少走了不少弯路,编写程序显得更为轻松和从容了。总结我的程序,我也发现仍有很多不足,也是我接下来需要努力的方向。一、程序现在运行的不是很流畅,魔方层面旋转时会有一点卡。我也不知道是不是正常现象,能不能加以改进。二、程序运行的背景有点单调,我原本想在后面加上背景图片的,由于时间关系,我没有加图片。三、我现在的屏幕保护效果不是很令人满意,我想做成有很多小立方体在屏幕上自由反弹,相互碰撞的效果。虽然看起来有点难,但是我会继续研究这个程序,争取能够实现预期的效果。21参考文献1(美)施瑞奈尔.OpenGL 编程指南.第七版.2012 年, 第 5 次:第 5 页.2刘正林. 周纯杰编著.最新 c 语言程序设计教程.武汉:华中科技大学出版社,20033谭浩强编著.c 语言程序设计 .北京:清华大学出版社,2002