很久之前的代码了,项目用的是vs2013,编译器套件可以在项目属性中修改。
代码地址:点击打开链接
模型导入
此处使用了开源代码。
自己使用Maya对场景中一些物体建模,然后三角形化后保存为obj格式,再经过特殊处理后从代码中加载。
为了提高渲染速度,使用了显示列表。
纹理映射
此处使用了开源代码,支持jpg,bmp,png等图片的导入。
包围盒
为了使场景看起来更加完整,使用了包围盒技术。
由于网上无法找到与雪地相关很好的素材(大多是普通的天空包围盒),所以只能手工构造一个粗糙的映射。
如下图,蓝色区域为原图,我们按红色区域部分将纹理映射到一个立方体上。其中中间四个矩形分别对应着立方体的四个面。而上面四个三角形和下面四个三角形分别映射到立方体的顶部和底部。
这样的做法会导致边界感较明显,并且在图像的左右侧拼接在一起时有明显的破绽,这只能通过限制视角来避免。
拾取
为了提供良好的交互体验,在这里利用opengl拾取操作,能让用户选择点击一定物体,并且给出反馈。
漫游
按下wsad可控制前后左右运动,按下ikjl可控制方向向上下左右变化。
通用粒子系统引擎
粒子系统引擎是三维的,独立编写完成的。
之所以称之为通用的,是因为在这个类架构的基础上,只需要修改一些物理参数以及贴图/模型/颜色等美术素材,就能实现不同的效果。基于这个粒子系统引擎,我完成了飘雪,落叶(面发射,自由落体),喷泉(点发射,y轴上初速度向上,自由落体),火焰(点发射,y轴上初速度向上),烟花(点发射,初速度向四周,自由落体)
但也正是由于“通用性”,这个代码的可扩展性比较弱,也就是说,如果想对粒子进行一些特殊的操作,原来的类并没有提供过多的接口,但基本功能已经可以实现了。
粒子系统先是参考了Maya中粒子系统的效果,在里面有两个可以设置的对象,一个是粒子的属性,另一个是发射器属性,参考这一特性,在代码编写中,也分为了两个类,一个是粒子类,一个是发射器类。
粒子的属性:
位置 |
vec place |
大小 |
vec size |
速度 |
vec speed |
加速度 |
vec acc |
角度 |
vec angle |
颜色 |
vec color |
生命 |
float life |
纹理 |
unsigned int texture |
绘制 |
void draw() void draw(GLuint texture) |
显示 |
void show() |
更新 |
void update() |
发射器的属性:
发射速率 |
int speed |
发射器位置 |
float x1,y1,x2,y2,z1,z2 |
粒子 |
particle **p |
初始化粒子 |
particle* (*f)() |
粒子是否死亡 |
bool (*isDead)(particle*) |
粒子发射 |
void emit(particle* (init)(), bool(*judge)(particle*)); void emit(particle* (init)()); |
更新粒子 |
void update(); |
重设粒子 |
void reset(); |
粒子消逝 |
void fade(); |
显示 |
void show(); |
为了完成可扩展性,使用了函数指针注入类的方法,类似于Maya中自己编写脚本过程。可提供编写的程序包括:
init_particle 粒子参数的初始化。
judge 判断粒子的生命周期。
粒子系统基本流程:
初始化发射器
初始化粒子
绘制粒子
更新粒子位置
粒子死亡,生成新粒子
具体到代码中,就是先新建一个发射器,设定上表中的参数;然后在发射器中新建粒子,初始化粒子的函数是作为参数传给发射器的。
其中,发射器可以通过设置x1,y1,z1,x2,y2,z2,来控制发射范围。例如,若设置x1==x2,y1==y2,z1==z2,则最终得到的是点发射。发射位置在指定范围内线性插值,但对于圆弧形状的发射,可能存在角度上非线性的缺陷。
接下来每一帧都绘制粒子,隔了一定时间更新粒子位置(利用加速度更新速度,利用速度更新位置),每更新一次,寿命减少一定值(或者让用户指定粒子消亡的条件)。
当粒子寿命减为0时,再重新发射一个粒子。这样就保证了粒子数目维持在稳定的数量。
雪花飞舞
控制雪花飞舞需要从“天空”进行面发射,然后设定y方向上的重力加速度,以及x,z方向上轻微的扰动加速度。随机指定雪花大小、方向等特性。
在这里,控制雪花达到一定寿命,或雪花落到地面上时,雪花消亡,并生成新雪花。
如果雪花落到了地面,那么将其交由另一个类来管理,该类将继续维护雪花的当前位置。雪花将在地面上停留一小段时间,然后自动消亡。
考虑不让雪花自动消亡,那么会出现这样的状况:雪花在地面上越积累越多,渲染压力变大,最终速度变得缓慢,直到错误崩溃。所以有必要让地面上的雪花也维持数量上的平衡。
另外一种可能的做法是维护地上的雪花量,然后通过置换贴图等方法来制造积雪越来越多的假象。
按下i键,控制雪花开始降落,按下k键,控制雪花不再继续产生。
火焰燃烧
火焰燃烧控制粒子从比较小的面发射,初速度Y轴方向上是沿正向的,其余的随机设很小的量。
群组动画
使用了路径跟随效果。
蝴蝶自身在上下扇动翅膀运动,同时,也在按照指定路径向前运动,存在一个灰色区域,蝴蝶运动的区域不会超过这个范围。
当一只蝴蝶飞到一定位置时,它通知下一只蝴蝶开始运动;直到飞舞的蝴蝶达到一定个数。最终得到的效果就是一群蝴蝶扇动着翅膀沿着指定路径运动,但又不完全按照路径,有着随机的偏移量。
路径是通过样条曲线计算得到的。
同样地,维护两个类,第一个类是蝴蝶类,它描述的是单只蝴蝶的属性:
float x; |
位置x |
float y; |
位置y |
float z; |
位置z |
float dx; |
偏移x |
float dy; |
偏移y |
float dz; |
偏移z |
float spinz; |
翅膀旋转 |
float spinzi; |
翅膀旋转偏移 |
float flap; |
翅膀位置 |
float fi; |
翅膀位置偏移 |
int index; |
当前路径下标 |
spline* line; |
样条曲线 |
int texture[3]; |
纹理 |
void set(int _index); |
设置属性 |
void update(); |
更新 |
void show(); |
显示 |
第二个类是蝴蝶管理类,它描述的是蝴蝶群体的属性:
spline* line; |
曲线 |
point *p; |
插值点 |
bool isGen; |
是否生成路径 |
vector |
蝴蝶群体 |
void genPath(); |
生成路径 |
按下z键,可以显示飞舞的蝴蝶,按下x键,则不显示。
碰撞检测
碰撞检测使用的是最为简单的AABB包围盒。
在这里实现了两种碰撞检测,第一个是人在包围盒内,不能走出包围盒;第二个是人在包围盒外,不能走入包围盒。在这个场景中,对应的分别是天空包围盒和房子外的包围盒。
在漫游中,我们维护了两个量,一个是视点(眼睛看向的地方),另一个是观察点(眼睛所在的位置),我们需要同时保证这两个不落到包围盒内(外)。
对于第一种包围盒,我们一检测到视点或观察点超过包围盒范围,那么就立即将其修改到正常范围,同时我们监测两点之间的距离,一旦小于某一特定值,立即将其修改。因为在前面的修正过程中很可能让视点和观察点的位置发生错误。
视点超过范围,一般是在前进的时候;观察点超过范围,一般是在后退的时候。
由于这一方法是单独修改某一方向的值,所以修改之后角度会发生偏移,这一偏移是逐渐往与墙壁平行方向延伸的,最终达到自动修正角度的效果。
对于第二种包围盒,我们将监测点与视点连成的线与包围盒进行相交检测。如果相交,那么通过数学计算,恢复原来的位置即可。