原文地址:http://www.videotutorialsrock.com/opengl_tutorial/animation/home.php
视频下载:http://www.videotutorialsrock.com/opengl_tutorial/animation/video.flv
文本格式:http://www.videotutorialsrock.com/opengl_tutorial/animation/text.php
源码下载:http://www.videotutorialsrock.com/opengl_tutorial/animation/animation.zip
第九课 动画
3D动画
程序中加入3D动画将会非常的棒。有许多创建3D动画的方法,我们将使用帧(frames)来制作。我们将有一个外部文件以存储我们模型在一个循环中特定时间,特定顶点的位置。在一个特定时间绘制模型,我们将使用靠近这时间的2帧并取梭鱼顶点的平均位置,就是说,我们将要在这两帧中插入一帧。有一些更加方便的方法来绘制动画,特别是骨架型动画(skeletal animation)。这里我们专注于简单直白的方法。本课要比前几课复杂。
存储和加载3D动画
表示3D动画有很多文件格式。这里使用MD2(雷神之锤2的格式)。Quake 2可能有些旧,但使用MD2是因为文件格式简单和公开,网络上也有其他人制作了许多md2文件。
另一个使用md2的原因是Blender,这是一个开源的3D模型软件,可以导出成MD2文件。当然专业人士通常使用3d max和 Maya来进行3d建模。由于这些软件非常贵,我这里就用Blender了。
遗憾的是,目前Blender的MD2导出器不是很好用,经常崩溃出错。我不能将动画导出为2.44的目前最新版本,所以得用2.42版。希望以后版本的Blender会改进这个导出器。
使用Blender,我制作了一个3D人物模型,包括他的纹理。我们的程序是让这个人像下面一样走路:
在Blender中编辑的3D模型如下:
加载和使用md2文件
现在我们已经为我们的模型制作了md2文件,我们需要载入并使用这个文件。我上网查了md2文件格式,因此我知道如何去加载,下面会具体示范。
将所有和md2文件有关的代码都放在md2model.h和md2model.cpp文件中。我们用一个MD2Model类存储所有和动画及其绘制相关的信息。md2model.h的代码:
struct MD2Vertex {
Vec3f pos;
Vec3f normal;
};
struct MD2Frame {
char name[16];
MD2Vertex* vertices;
};
struct MD2TexCoord {
float texCoordX;
float texCoordY;
};
struct MD2Triangle {
int vertices[3]; //The indices of the vertices in this triangle
int texCoords[3]; //The indices of the texture coordinates of the triangle
};
首先,MD2Model类中将使用一些结构体,包括顶点,帧,纹理坐标,和三角形。每个帧都有一个名字,通常表示动作的类型(e.g. "run", "stand")。这些帧只是使用一个顶点数组来保存每个顶点的位置和向量。每帧都有相同的顶点数量,所以帧1中的顶点5对应帧2中的顶点5,只是顶点的位置不同。三角形是用帧中的顶点数组的索引号和MD2Model类中出现的纹理坐标的索引号表示的。
class MD2Model {
private:
MD2Frame* frames;
int numFrames;
MD2TexCoord* texCoords;
MD2Triangle* triangles;
int numTriangles;
这是我们需要绘制模型的主要代码。我们有帧数组,纹理坐标数组,和三角形数组。
GLuint textureId;
这是我们绘制动画的纹理id。
int startFrame; //The first frame of the current animation
int endFrame; //The last frame of the current animation
这是动画的开始和结束帧。
/* The position in the current animation. 0 indicates the beginning of
* the animation, which is at the starting frame, and 1 indicates the
* end of the animation, which is right when the starting frame is
* reached again. It always lies between 0 and 1.
*/
float time;
额……,还是自己看注释吧……
MD2Model();
public:
~MD2Model();
这是构造和析构函数。因为只有一个特殊的MD2Model方法才可以创建MD2Model的对象,所以这里的构造函数是私有的。
//Switches to the given animation
void setAnimation(const char* name);
因为md2文件实际上可以在一定范围的帧中存储几个动画,这个函数让我们设置当前动画。比如我们的动画占居40到45帧,给一个名字可以正确的识别到,这后面将会看到。
//Advances the position in the current animation. The entire animation
//lasts one unit of time.
void advance(float dt);
这个函数让动画走向后面的状态。不停的调用这个函数,可以让3D人物在不同的位置上。
//Draws the current state of the animated model.
void draw();
这是实际的绘制函数。
//Loads an MD2Model from the specified file. Returns NULL if there was
//an error loading it.
static MD2Model* load(const char* filename);
};
load函数加载一个md2文件。这是一个静态函数,通过MD2Model::load("somefile.md2")调用,不需要一个对象。load看上去基本上是一个普通的函数,只是可以访问私有成员。
这是md2model.h文件,现在来看cpp文件。
namespace {
//...
}
一点C++的技巧:将所有的非类的常数和函数放在namespace{}的代码块中,可以在不同的文件中定义相同名字的常量和函数而不引起冲突。比如在这个namespace块和main.cpp中都可以有一个叫foo的函数。
//Normals used in the MD2 file format
float NORMALS[486] =
{-0.525731f, 0.000000f, 0.850651f,
-0.442863f, 0.238856f, 0.864188f,
//...
-0.688191f, -0.587785f, -0.425325f};
MD2使用了162个特殊的向量,并只需给出这些向量的索引来存储向量,并不直接存储。这个数组包括了所有MD2将要使用的向量。
当我们加载文件时,有一个烦人的小细节是文件字节序的问题。当设计CPU时,设计师需要决定将最高位放在开始还是最后。比如,short int型258=1(256)+2,当最高位在前面时可以存为字节(1,2),当最低位在前面时可以存为(2,1)。第一种存储方式叫做"big-endian"(二进位资料次序);第二种叫做"little-endian"。所以,设计cpu的人2种方式都使用了。一些CPU,包括奔腾,按照little-endian方式存储数据;一些其他的CPU按照big-endian方式存储数据。虽然看起来字节序是很多抨击的对象,但还需要对两种方式进行考虑,这个问题困扰了计算机程序员祖宗十八代了……
这带来什么问题呢?当一个整数需要在MD2文件中存储为几个字节的时候问题出现了。文件是按照little-endian方式存储的,但是加载文件的计算机不一定是little-endian方式载入的。因此当载入文件的时候,我们需要特别的小心,确保程序运行的计算机不会出现字节序的问题。
//Returns whether the system is little-endian
bool littleEndian() {
//The short value 1 has bytes (1, 0) in little-endian and (0, 1) in
//big-endian
short s = 1;
return (((char*)&s)[0]) == 1;
}
这个函数检查我们的系统是使用little-endian还是big-endian的。如果短整型1的第一个字节为1,那我们的机器是little-endian的;否则就是big-endian的。
//Converts a four-character array to an integer, using little-endian form
int toInt(const char* bytes) {
return (int)(((unsigned char)bytes[3] << 24) |
((unsigned char)bytes[2] << 16) |
((unsigned char)bytes[1] << 8) |
(unsigned char)bytes[0]);
}
//Converts a two-character array to a short, using little-endian form
short toShort(const char* bytes) {
return (short)(((unsigned char)bytes[1] << 8) |
(unsigned char)bytes[0]);
}
//Converts a two-character array to an unsigned short, using little-endian
//form
unsigned short toUShort(const char* bytes) {
return (unsigned short)(((unsigned char)bytes[1] << 8) |
(unsigned char)bytes[0]);
}
这里有一些函数将一序列的字节转换成int, short 或者 unsigned short数据。他们使用<<移位操作符,这在数字末尾将补0。比如,二进制数1001101移位5就是100110100000。前面任何额外的bit都将被丢弃。注意函数没有管运行的机器的字节序问题。
//Converts a four-character array to a float, using little-endian form
float toFloat(const char* bytes) {
float f;
if (littleEndian()) {
((char*)&f)[0] = bytes[0];
((char*)&f)[1] = bytes[1];
((char*)&f)[2] = bytes[2];
((char*)&f)[3] = bytes[3];
}
else {
((char*)&f)[0] = bytes[3];
((char*)&f)[1] = bytes[2];
((char*)&f)[2] = bytes[1];
((char*)&f)[3] = bytes[0];
}
return f;
}
连浮点数都逃脱不了字节序的问题。要转换4个字节的浮点数,我们需要检查我们的机器是否是little-endian的,并恰当的设置好每个字节。