这学期真的忙到爆炸orz,已经很久没有更新了。这篇文章是关于计算机图形学的课程设计——实现三维美术馆漫游。其实与其说是图形学的课设,不如说是虚拟现实技术的入门,下图是课设的总体要求:
目录
一. 实验目的
- 掌握三维应用软件基本开发流程。
- 掌握基本三维建模软件Blender的基本建模方法。
- 掌握虚拟现实引擎WorldViz Vizard呈现三维场景的方法。
- 掌握三维模型呈现软件Assimp预览三维模型的方法。
二. 开发环境
- 系统环境:Win10
- 三维建模软件:Blender
- 三维呈现软件:Vizard inspector、Assimp
- 实时渲染软件:WorldViz Vizard
- 所用语言:Python 3.8(Vizard支持)
三. 设计思路
本课程设计由四个主要模块所构成,分别是:虚拟三维时钟模块、两个谈话小人模块、第三人称漫游模块、三维物体放置模块。各模块设计思路如下:
(一)虚拟三维时钟模块
1. 三维时钟模型的设计
三维时钟的设计步骤如下:首先用一个圆柱体和三个长方体分别设计出表盘和时针、分针和秒针。对于表盘的刻度,每一个刻度都用一个小的立方体来表示,12个刻度将表盘等分为12份。而对于3,6,9,12这四个时间刻度,则用四个小的圆柱体来刻画,以便突出。制作完三位时钟模型后,将表盘组合体、时针、分针、秒针分别导出为biaopan、shizhen、fenzhen、miaozhen的obj格式文件,并将obj文件保存到resourse文件夹。加载三维时钟的代码如下所示。
#加载时钟三维模型
biaopan = viz.add('biaopan.obj') #时钟表盘
shizhen = viz.add('shizhen.obj') #时 针
fenzhen = viz.add('fenzhen.obj') #分 针
miaozhen = viz.add('miaozhen.obj') #秒 针
表盘组合体和时针、分针、秒针的截图如下:
2. 三维时钟模型的放置
首先利用setScale()函数调整时钟模型大小,并利用setPosition()和setEuler()函数设置表盘和各个指针的初始放置位置和初始朝向。相关代码如下所示。
#调整时钟模型大小
biaopan.setScale([0.25,0.25,0.25]) #调整模型大小
shizhen.setScale([0.25,0.25,0.25]) #调整模型大小
fenzhen.setScale([0.25,0.25,0.25]) #调整模型大小
miaozhen.setScale([0.25,0.25,0.25]) #调整模型大小
#设置时钟初始位置
biaopan.setPosition(0,4,10) #表盘坐标位置
shizhen.setPosition(0,4,10) #时针坐标位置
fenzhen.setPosition(0,4,10) #分针坐标位置
miaozhen.setPosition(0,4,10) #秒针坐标位置
#设置时钟初始朝向
biaopan.setEuler(0,-90,0) #表盘初始朝向
shizhen.setEuler(-90,180,0) #时针初始朝向
fenzhen.setEuler(-90,180,0) #分针初始朝向
miaozhen.setEuler(-90,180,0) #秒针初始朝向
3. 获取系统时间并处理
编写三维时钟的实现函数Clock(),首先利用datetime库中的now()函数获取系统的实时时间t。由于系统时间是实时变化的,所以必须将t.hour、t.minute、t.second的值赋值给其他变量hour、minute、second才能对其进行处理。并且由于时钟的一圈只有12小时,所以对于12:00 - 24:00的情况则需要对hour进行模12除法的处理。相关代码如下所示。
#【函数】时钟渲染
def Clock():
t = datetime.datetime.now() #获取系统时间(系统时间是实时变化的,必须将其值赋值给其他变量才可以处理)
hour=t.hour #将系统的小时值赋值给hour
minute=t.minute #将系统的分钟值赋值给minute
second=t.second #将系统的秒数值赋值给second
hour=hour%12 #时钟一圈只有12小时,对于12:00~24:00的情况需要对hour进行模12除法的处理
4. 表针旋转角度的计算
利用时间存在1 minute = 60 second、1 hour = 60 minute的换算关系,可以推出秒针、分针、时针的旋转角度计算公式:
秒针的旋转角度等于当前秒数占一分钟60s的比例再乘以360°。
分针的旋转角度等于整分钟的个数对应的角度与不足一分钟的秒数所对应的角度之和。由于分针每分钟分针转6°,所以分针的旋转角度等于6°与多余的分钟数之积。
时针的旋转角度等于整小时的个数对应的角度与不足一小时的分钟数所对应的角度之和。由于时针每过12小时转360°,即1小时转30°,故整小时个数所对应的旋转角度等于hour/12 * 360°,不足一小时所对应的旋转角度等于minute/60*30°。
相关计算公式及代码如下所示。
second_angle=second/60.0*360 #计算一秒钟秒针的旋转角度
minute_angle=minute/60.0*360+second/60.0*6 #计算一秒钟分针的旋转角度
hour_angle=hour/12.0*360+minute/60.0*30 #计算一秒钟时针的旋转角度
5. 表针相对于圆心偏移的计算
圆心坐标为(0,4,10),由于需要实现时钟的功能,故每隔一秒钟vizact.ontimer()函数便需要刷新一次画面。每过一秒钟,表盘上的秒针、分针、时针的位置都会相对于圆心偏移一定的值,因此需要利用setEuler()函数实时更新表针的欧拉角。秒针每60s转360°,分针每60分钟转360°,时针每12小时转360°。因为顺时针旋转,故表针每过一秒其俯仰角θ都应该减去相应的上一步计算所得的角度数△θ。相关计算公式及代码如下所示。
miaozhen.setEuler(-90,180-second_angle,0) #设置一秒钟后的秒针角度
fenzhen.setEuler(-90,180-minute_angle,0) #设置一秒钟后的分针角度
shizhen.setEuler(-90,180-hour_angle,0) #设置一秒钟后的时针角度
6. 三维时钟模型的实现
完整的时钟渲染函数如下:
#【函数】时钟渲染
def Clock():
t = datetime.datetime.now() #获取系统时间(系统时间是实时变化的,必须将其值赋值给其他变量才可以处理)
hour=t.hour #将系统的小时值赋值给hour
minute=t.minute #将系统的分钟值赋值给minute
second=t.second #将系统的秒数值赋值给second
hour=hour%12 #时钟一圈只有12小时,对于12:00~24:00的情况需要对hour进行模12除法的处理
second_angle=second/60.0*360 #计算一秒钟秒针的旋转角度
minute_angle=minute/60.0*360+second/60.0*6 #计算一秒钟分针的旋转角度
hour_angle=hour/12.0*360+minute/60.0*30 #计算一秒钟时针的旋转角度
miaozhen.setEuler(-90,180-second_angle,0) #设置一秒钟后的秒针角度
fenzhen.setEuler(-90,180-minute_angle,0) #设置一秒钟后的分针角度
shizhen.setEuler(-90,180-hour_angle,0) #设置一秒钟后的时针角度
vizact.ontimer(1,Clock) #每隔一秒刷新一次,实现时钟功能
执行程序后,三维时钟模型的实现截图如下所示。
(二)两个谈话小人模块
- 加载vcc_male2.cfg和vcc_female.cfg人物模型源文件,对象命名为avater_talking_male和avater_talking_female。并通过设置参数pos和euler设置人物的位置和朝向。
avatar_talking_male = viz.addAvatar('vcc_male2.cfg', pos=(1,0,5), euler=(90,0,0)) #谈话人1
avatar_talking_female = viz.addAvatar('vcc_female.cfg',pos=(2,0,5), euler=(-90,0,0)) #谈话人2
- 使用Vizard inspector预览动画。其中,vcc_male2.cfg的内嵌谈话动画male_talk.cafx的编号为4,vcc_female.cfg的内嵌谈话动画femFlat_high_talk.cafx的编号为14。
- 使用avatar.state()函数在Vizard中播放动画,效果如下所示。
avatar_talking_male.state(4) #加载谈话人1的谈话动作,动作4
avatar_talking_female.state(14) #加载谈话人2的谈话动作,动作14
(三)第三人称漫游模块
1. 第三人称摄像机计算公式
第三人称摄像机的特点是始终位于人物的后上方,摄像机view位于人物的后上方才能够以第三人称呈现人物的形象,并且还需要保持摄像机永远对准人物的正后方,需要和人物一同旋转。
设人物的坐标为p(p[0],p[1],[2]),摄像机的坐标为p’(p’[0],p’[1],p’[2])。其中p[0]、p[1]、p[2]分别表示在x轴、y轴、z轴上的坐标。由于摄像机位于人物的上方,故p’[1]=p[1] + h,经过试验h=2时效果最佳,即摄像机比人物高2个单位长度。
当人物发生旋转时,摄像机跟随人物进行旋转。设旋转角为θ,由于摄像机位于人物的后方,所以其在x轴和z轴的坐标与人物的不同。由下图可知,摄像机在x轴方向偏移了-sin(θ),在z轴方向偏移了-cos(θ)。
依据此公式得到的相关代码如下所示:
p[1] = p[1]+2 #摄像机在y轴的相对高度比人物高,即在y轴方向摄像机以人物高2个单位
p[0] = p[0]-3*math.sin(math.pi*(e[0]/180)) #摄像机在x轴的相对距离在人物后,即摄像机在x轴方向偏移了3sin(θ)个单位
p[2] = p[2]-3*math.cos(math.pi*(e[0]/180)) #摄像机在z轴的相对距离在人物后,即摄像机在z轴方向偏移了3cos(θ)个单位
view.setPosition(p) #设置移动后的摄像机坐标
2. 利用鼠标实现旋转的原理
首先定义位置坐标p1、旋转前人物的欧拉角e1和旋转前摄像机的欧拉角e2。由于鼠标对于视角产生影响的移动分为横向移动和纵向移动两类,即视角左右旋转和视角上下旋转,所以需要分开讨论计算。
视角横向旋转的情况较为简单。由于鼠标灵敏度问题的存在,故在计算旋转量时需要除以一个常数来控制鼠标的灵敏度从而达到最佳效果,经过试验该常数为20时能达到较好效果。由于为横向旋转,故偏航角e1[0]改变。代码如下:
e1[0] = e1[0]+e.dx/20 #鼠标横向偏移,将鼠标偏移量除以20控制鼠标灵敏度,人 物旋转,偏航角e1[0]改变
avatar_me.setEuler(e1) #旋转后人 物的朝向欧拉角
视角纵向旋转的情况较为复杂,由于人物无法通过纵向旋转看到后方,所以需要加一个角度判断。依据旋转角度的绝对值的取值,分旋转角度绝对值小于90°和大于90°两类情况进行处理。当旋转角的绝对值小于90°时旋转可行,相应的摄像机也随之纵向旋转,俯仰角e2[1]改变。当旋转角的绝对值大于90°时旋转不可行,此时摄像机的俯仰角不变,不发生视角上下移动。代码如下:
e2[1] = e2[1]-e.dy/20 #鼠标纵向偏移,将鼠标偏移量除以20控制鼠标灵敏度,摄像机旋转,俯仰角e2[1]改变
if(e2[1]>-90 and e2[1]<90): #俯仰角e2[1]的绝对值小于90°的情况,此时视角上下移动处于范围内
view.setEuler(e1[0],e2[1],e1[2]) #旋转后摄像机的朝向欧拉角,俯仰角改变,发生视角上下移动,欧拉角与e1不同
else: #俯仰角e2[1]的绝对值大于90°的情况,此时视角上下移动不在范围内
view.setEuler(e1) #旋转后摄像机的朝向欧拉角,俯仰角不变,没有视角上下移动,欧拉角与e1相同
用鼠标实现第三人称摄像机旋转的完整代码如下:
#【函数】鼠标控制第三人称摄像机跟随人物旋转
def onm ouseMove(e):
p1 = avatar_me.getPosition() #摄像机位置坐标
e1 = avatar_me.getEuler() #旋转前人 物的朝向欧拉角
e2 = view.getEuler() #旋转前摄像机的朝向欧拉角
e1[0] = e1[0]+e.dx/20 #鼠标横向偏移,将鼠标偏移量除以20控制鼠标灵敏度,人 物旋转,偏航角e1[0]改变
avatar_me.setEuler(e1) #旋转后人 物的朝向欧拉角
e2[1] = e2[1]-e.dy/20 #鼠标纵向偏移,将鼠标偏移量除以20控制鼠标灵敏度,摄像机旋转,俯仰角e2[1]改变
if(e2[1]>-90 and e2[1]<90): #俯仰角e2[1]的绝对值小于90°的情况,此时视角上下移动处于范围内
view.setEuler(e1[0],e2[1],e1[2]) #旋转后摄像机的朝向欧拉角,俯仰角改变,发生视角上下移动,欧拉角与e1不同
else: #俯仰角e2[1]的绝对值大于90°的情况,此时视角上下移动不在范围内
view.setEuler(e1) #旋转后摄像机的朝向欧拉角,俯仰角不变,没有视角上下移动,欧拉角与e1相同
view.setPosition(p1) #设置旋转后摄像机的位置坐标
viz.callback(viz.MOUSE_MOVE_EVENT,onMouseMove) #回调函数检测鼠标偏移量
3. 利用键盘实现行走的原理
首先定义人物的位置坐标p和朝向欧拉角e。键盘控制人物移动有五种情形:不按按键原地不动、按W键控制前进、按S键控制后退、按A键控制左移、按D键控制右移。五种情况需要分开进行分析设计:
若不按按键原地不动,则只需要播放人物不动时的动画即可,代码如下:
if not (viz.key.isDown('w') or viz.key.isDown('s') or viz.key.isDown('a') or viz.key.isDown('d')): #没有按下方向键
avatar_me.state(1) #人物不动,播放内嵌动画male_high_idle1.cafx
若需要控制前进、后退、左移、右移,则除了需要播放相应动画以外,还需要计算人物在x轴和z轴两个方向的偏移量,从而实现人物在空间上的移动。偏移量需要乘上一个比例系数,以免移动过快或过慢。
人物位置移动的计算公式如下:
用键盘实现人物移动的代码如下:
elif viz.key.isDown('w'): #按下W键,人物前进
avatar_me.state(2) #人物向前走,播放内嵌动画2-male_high_walk.cafx
p[0] += 0.05*math.sin(math.pi*(e[0]/180)) #人物在x轴方向偏移了sin(θ),将相对偏移量乘上系数0.05控制移动速度
p[2] += 0.05*math.cos(math.pi*(e[0]/180)) #人物在z轴方向偏移了cos(θ),将相对偏移量乘上系数0.05控制移动速度
avatar_me.setPosition(p) #设置移动后新的人物坐标
elif viz.key.isDown('s'): #按下S键,人物后退
avatar_me.state(2) #人物向后退,播放内嵌动画2-male_high_walk.cafx
p[0] -= 0.05*math.sin(math.pi*(e[0]/180)) #人物在x轴方向偏移了-sin(θ),将相对偏移量乘上系数0.05控制移动速度
p[2] -= 0.05*math.cos(math.pi*(e[0]/180)) #人物在z轴方向偏移了-cos(θ),将相对偏移量乘上系数0.05控制移动速度
avatar_me.setPosition(p) #设置移动后新的人物坐标
elif viz.key.isDown('a'): #按下A键,人物左移
avatar_me.state(12) #人物向左移,播放内嵌动画12-male_high_strafeleft.cafx
p[0] -= 0.03*math.sin(math.pi*(e[0]/180)+90) #人物在x轴方向偏移了-sin(θ+π/2),将相对偏移量乘上系数0.03控制移动速度
p[2] -= 0.03*math.cos(math.pi*(e[0]/180)+90) #人物在z轴方向偏移了-sin(θ+π/2),将相对偏移量乘上系数0.03控制移动速度
avatar_me.setPosition(p) #设置移动后新的人物坐标
elif viz.key.isDown('d'): #按下D键,人物右移
avatar_me.state(13) #人物向右移,播放内嵌动画13-male_high_straferight.cafx
p[0] -= 0.03*math.sin(math.pi*(e[0]/180)-90) #人物在x轴方向偏移了-sin(θ+π/2),将相对偏移量乘上系数0.03控制移动速度
p[2] -= 0.03*math.cos(math.pi*(e[0]/180)-90) #人物在x轴方向偏移了-sin(θ+π/2),将相对偏移量乘上系数0.03控制移动速度
avatar_me.setPosition(p) #设置移动后新的人物坐标
4. 人物行走动画的程序逻辑
实现人物行走动画的程序逻辑为(以人物前进为例):首先获取键盘信息,当按下键盘的同时播放前进动画,紧接着分别计算移动后人物的x轴和z轴坐标值,之后设置人物坐标至新的坐标点,从而完成行走逻辑。
(四)其他三维物体模块
1. 漫步鸽子
- 使用Vizard inspector预览鸽子源文件pigeon.cfg,得知内嵌动画pigeon_walk.cafx可以实现鸽子的行走。
- 编写鸽子漫步运动逻辑。首先调用内嵌动画pigeon_walk.cafx,之后编写实现鸽子漫步的函数PigeonWalk()。首先定义walk1和walk2两个动作,walk1实现鸽子从初始点走到点(2,0,3),walk2实现鸽子从点(2,0,3)走回到初始点,再分别调用两次ontimer函数就可以得到鸽子来回走动的效果。相关代码及效果截图如下所示。
#【函数】鸽子漫步
def PigeonWalk():
walk1 = vizact.walkTo([2,0,3]) #鸽子走到坐标点(2,0,3)
vizact.ontimer(0.01,avatar_pigeon.addAction,walk1) #全局时间控制,无限次调用walk1
walk2 = vizact.walkTo([1,0,1]) #鸽子走到坐标点(1,0,1)
vizact.ontimer(0.01,avatar_pigeon.addAction,walk2) #全局时间控制,无限次调用walk2
PigeonWalk() #调用鸽子漫步函数,实现鸽子的来回漫步
2. 其他三维物体
本项目添加了两种三维物体,分别是自带的plant.osgb和第三方下载的FlowerVase.obj。其中plant.osgb文件为Vizard自带资源,直接调用即可:
#引入非第三方三维模型plant
Plant = viz.add('plant.osgb') #加载模型
Plant.setPosition(-1,0,2) #模型位置
对于第三方三维模型资源文件FlowerVase.obj,下载解压后用Inspector打开后即可预览FlowerVase三维图像,如下图所示。
将此obj文件另存为FlowerVase.osgb,路径为Vizard安装的resourse文件夹。并加上这段代码在场景中呈现此第三方花瓶资源:
#引入 第三方三维模型FloewrVase
FlowerVase = viz.add('FlowerVase.osgb') #加载模型
FlowerVase.setScale([2,2,2]) #模型大小
FlowerVase.setPosition(-1,0.5,1) #模型位置
FlowerVase.setEuler(0,0,0) #模型方向
运行后即可加载自带的plant模型和第三方FlowerVase模型: