程序员爱装B

写装A程序 做装C的事情

[搬家文] 第九课 动画-part 1

原文地址: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的,并恰当的设置好每个字节。

posted on 2010-07-19 20:29 camel 阅读(227) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

<2024年9月>
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

统计

常用链接

留言簿

随笔档案

文章档案

搜索

最新评论

阅读排行榜

评论排行榜