osgAnimation之动画基础篇

本文介绍了osgAnimation动画库的基础组件,包括关键帧、插值器、采样器、动画频道和动画类。详细解析了各组件的工作原理及其实现方式。
  • 简介

osgAnimation是osg库中提供场景动画效果的一个类库,它为我们提供了许多与场景动画相关的类,比如关键帧、插值、采样、频道、骨骼动画、材质变化等。本课就对osgAnimation库中的基础类进行一些解析。以下都是我个人学习过程中的一些记录和体会,方便以后自己复习之用。

  • 开始

  • Keyframe

对应文件 osgAnimation/keyframe

学习osgAnimation库,首先需要理解关键帧的含义。关键帧顾名思义是对应某一时刻动画中的一种状态,就像制作动画片一样。我们知道早期的动画片是作者一页一页画出来的,在通过迅速地切换让我们感受到了动态的效果,在这里关键帧就相当于其中的一页画面。在osgAnimation中关键帧定义如下:

    class Keyframe
    {
    public:
        double getTime() const { return _time; }
        void setTime(double time) { _time = time; }

    protected:
        double _time;

    };
很简单是吧,你可能回想:这里面根本什么都没有啊!时间应该对应一个内容啊。由于对应的内容千变万化(可能是运动位置、颜色、角度等等),因此在派生类中采用模板的方式来处理,即:

    template <class T>
    class TemplateKeyframe : public Keyframe
    {
    protected:
        T _value;
    public:
        TemplateKeyframe () {}
        ~TemplateKeyframe () {}

        TemplateKeyframe (double time, const T& value)
        {
            _time = time;
            _value = value;
        }

        void setValue(const T& value) { _value = value;}
        const T& getValue() const { return _value;}
    };
现在有了和时间对应的值,为了方便管理,还需要定义一个存储值的容器,即:KeyframeContainer,同样还是用相同的方式定义如下:

    class KeyframeContainer : public osg::Referenced
    {
    public:
        KeyframeContainer() {}
        virtual unsigned int size() const = 0;
    protected:
        ~KeyframeContainer() {}
        std::string _name;
    };
    template <class T>
    class TemplateKeyframeContainer : public std::vector<TemplateKeyframe<T> >, public KeyframeContainer
    {
    public:
        TemplateKeyframeContainer() {}
        typedef TemplateKeyframe<T> KeyType;

        virtual unsigned int size() const { return (unsigned int)std::vector<TemplateKeyframe<T> >::size(); }
    };
可以看到该容器继承自std::vector,这样我们就可以采用push_back这样的方法往容器里面插入关键帧。
  • Interpolator

对应文件osgAnimation/Interpolator

有了关键帧之后,我们需要对关键帧之间的时间对应的值进行计算,这就是所谓的插值,定义插值的基类如下:

    template <class TYPE, class KEY>
    class TemplateInterpolatorBase
    {
    public:

		//KEY对应的是关键帧类型
        typedef KEY KeyframeType;
		//TYPE对应的是关键帧对应的值
        typedef TYPE UsingType;

    public:
        mutable int _lastKeyAccess;

        TemplateInterpolatorBase() : _lastKeyAccess(-1) {}

        void reset() { _lastKeyAccess = -1; }

		//通过时间time,计算出当前的索引值
		//也就是该时间在两个关键帧之间
        int getKeyIndexFromTime(const TemplateKeyframeContainer<KEY>& keys, double time) const
        {
            int key_size = keys.size();
            if (!key_size) {
                osg::notify(osg::WARN) << "TemplateInterpolatorBase::getKeyIndexFromTime the container is empty, impossible to get key index from time" << std::endl;;
                return -1;
            }
            const TemplateKeyframe<KeyframeType>* keysVector = &keys.front();
            for (int i = 0; i < key_size-1; i++)
            {
                double time0 = keysVector[i].getTime();
                double time1 = keysVector[i+1].getTime();

                if ( time >= time0 && time < time1 )
                {
                    _lastKeyAccess = i;
                    return i;
                }
            }
            return -1;
        }
    };
这个基类负责寻找到插值所需要的两帧,也就是与需要插值时刻相距最近的那两个关键帧。之后进行插值就相对简单,osg中定义了几种插值的方式:

    template <class TYPE, class KEY=TYPE>
    class TemplateStepInterpolator : public TemplateInterpolatorBase<TYPE,KEY>
    {
    public:

        TemplateStepInterpolator() {}
        void getValue(const TemplateKeyframeContainer<KEY>& keyframes, double time, TYPE& result) const
        {

            if (time >= keyframes.back().getTime())
            {
                result = keyframes.back().getValue();
                return;
            }
            else if (time <= keyframes.front().getTime())
            {
                result = keyframes.front().getValue();
                return;
            }

            int i = this->getKeyIndexFromTime(keyframes,time);
            result = keyframes[i].getValue();
        }
    };
StepInterpolator直接找到与time时刻相距最近那一帧的值,另外还有Linear(线性的插值)、SphericalLinear(球面的插值)、CubicBezier(贝塞尔插值)
  • Sampler

对应文件osgAnimation/Samper

有了关键帧和处理关键帧的插值算法,在osgAnimation中使用了Sampler(采样器)的方式将二者组合起来,其中的成员函数实现一目了然,都是调用插值器中的函数:

    //F实参化到时候需要传入的是一个Interpolator类
	template <class F>
    class TemplateSampler : public Sampler
    {
    public:
	
		//KeyframeType关键帧的类型
        typedef typename F::KeyframeType KeyframeType;
		//关键帧容器类型
        typedef TemplateKeyframeContainer<KeyframeType> KeyframeContainerType;
        //通过关键帧计算出的值的类型
		typedef typename F::UsingType UsingType;
        typedef F FunctorType;

        TemplateSampler() {}
        ~TemplateSampler() {}

        void getValueAt(double time, UsingType& result) const { _functor.getValue(*_keyframes, time, result);}
        void setKeyframeContainer(KeyframeContainerType* kf) { _keyframes = kf;}

        virtual KeyframeContainer* getKeyframeContainer() { return _keyframes.get(); }
        virtual const KeyframeContainer* getKeyframeContainer() const { return _keyframes.get();}

        KeyframeContainerType* getKeyframeContainerTyped() { return _keyframes.get();}
        const KeyframeContainerType* getKeyframeContainerTyped() const { return _keyframes.get();}
		
		//安全地得到一个关键帧容器,建议在程序中使用该方法
        KeyframeContainerType* getOrCreateKeyframeContainer()
        {
            if (_keyframes != 0)
                return _keyframes.get();
            _keyframes = new KeyframeContainerType;
            return _keyframes.get();
        }

        double getStartTime() const
        {
            if (!_keyframes || _keyframes->empty())
                return 0.0;
            return _keyframes->front().getTime();
        }

        double getEndTime() const
        {
            if (!_keyframes || _keyframes->empty())
                return 0.0;
            return _keyframes->back().getTime();
        }

    protected:
        FunctorType _functor;
        osg::ref_ptr<KeyframeContainerType> _keyframes;
    };
到这里我们已经可以将采样器应用到我们的程序中了,例如:自己定义更新回调,传入关键帧参数,根据关键帧计算每个时刻的值(比如物体姿态),并进行更新来达到动画效果。在osgAnimation中还进行了更高的封装,即Channel(动画频道的概念)
  • Channel

对应文件osgAnimation/Channel和osgAnimation/Channel.cpp

在一个Channel之中封装了采样器Sampler和执行对象Target,执行对象可以理解为将采样器计算的插值结果保存在这个对象之中,查看一下Target的实现如下:

  template <class T>
    class TemplateTarget : public Target
    {
    public:

        inline void lerp(float t, const T& a, const T& b);

		//TODO:怎么解释?
		// 以下是我的理解:
		 //如果多个Channel共享一个执行对象Target,那么
		 //在调用update的过程中,必须按照优先级的顺序进行
        void update(float weight, const T& val, int priority)
        {
            if (_weight || _priorityWeight)
            {
                if (_lastPriority != priority)
                {
                    _weight += _priorityWeight * (1.0 - _weight);
                    _priorityWeight = 0;
                    _lastPriority = priority;
                }

                _priorityWeight += weight;
                float t = (1.0 - _weight) * weight / _priorityWeight;
                lerp(t, _target, val);
            }
            else
            {
                _priorityWeight = weight;
                _lastPriority = priority;
                _target = val;
            }
        }
        const T& getValue() const { return _target; }
        void setValue(const T& value) { _target = value; }
    protected:
		//记录了最终的结果
        T _target;
    };
在Channel的实现中有同样有一个update成员函数,它的实现反应了Channel的作用,通过采样器计算得到Value值,然后再通过Target对象的更新update,最终将计算得到的结构存储在Target对象的成员变量_target之中以便后续使用。代码如下:osgAnimation/Channel

        virtual void update(double time, float weight, int priority)
        {
            // skip if weight == 0
            if (weight < 1e-4)
                return;
            typename SamplerType::UsingType value;
            _sampler->getValueAt(time, value); //得到采样器插值的值value
            _target->update(weight, value, priority);//对value进行加权计算,并将结果保存在target对象之中
        }
  • Animation

对应文件osgAnimation/Animation和osgAnimation/Animation.cpp

最后将这些频道整合起来的类是动画类Animation,代码如下:

    class OSGANIMATION_EXPORT Animation : public osg::Object
    {
    public:
        META_Object(osgAnimation, Animation)

        Animation() : _duration(0), _weight(0), _startTime(0), _playmode(LOOP) {}
        Animation(const osgAnimation::Animation&, const osg::CopyOp&);

        enum PlayMode
        {
            ONCE,
            STAY,
            LOOP,
            PPONG
        };
		
        void addChannel (Channel* pChannel);
        ChannelList& getChannels();
        const ChannelList& getChannels() const;
		
        void setDuration(double duration);
        void computeDuration();
        double getDuration() const;


        void setWeight (float weight);
        float getWeight() const;

        bool update (double time, int priority = 0);
        void resetTargets();

        void setPlayMode (PlayMode mode) { _playmode = mode; }
        PlayMode getPlayMode() const { return _playmode; }

        void setStartTime(double time)  { _startTime = time;}
        double getStartTime() const { return _startTime;}

    protected:
        double computeDurationFromChannels() const;
        ~Animation() {}

        double _duration;
        double _originalDuration;
        float _weight;
        double _startTime;
        PlayMode _playmode;
        ChannelList _channels;
    };
将许多Channel整合在了一起,实现的过程也是调用Channel中的成员函数来实现,很容易理解。在Animation中可以设置播放的模式,播放的模式实际上是通过这些模式来计算时间

    switch (_playmode)
    {
    case ONCE:
        if (t > _originalDuration)
            return false;
        break;
    case STAY:
        if (t > _originalDuration)
            t = _originalDuration;
        break;
    case LOOP:
        if (!_originalDuration)
            t = _startTime;
        else if (t > _originalDuration)
            t = fmod(t, _originalDuration);
        //      std::cout << "t " << t << " duration " << _duration << std::endl;
        break;
    case PPONG:
        if (!_originalDuration)
            t = _startTime;
        else
        {
            int tt = (int) (t / _originalDuration);
            t = fmod(t, _originalDuration);
            if (tt%2)
                t = _originalDuration - t;
        }
        break;
    }
以上就是osgAnimation库中基础部分的介绍,后续还会记录在实际操作中如何使用这些类来完成一个完整的动画。








本人主要从事图形图象工作,空闲之余接触了一些游戏编程,特写一些编程心得,本文 适合没有接触过人物动画编程的初学者,希望游戏制作的大虾们指点,交流。 在以前还有没接触人物动画编程的时候,觉得通过编程让人物动起来一定是一件很麻烦 的事情,尤其是初学者,大都会摸不着头脑,碰到诸如骨骼动画之类,似乎无从下手。但是 当你了解了它们的格式,就会发现其实真正的人物动画的制作并不是在编程阶段,而是在模 型构建阶段,程序员主要做工作的是掌握模型文件的格式,将存储在人物模型中的各种信息, 如顶点,面片,材质,骨骼或顶点运动的关键帧序列等信息读入内存然后用你熟悉的 SDK 绘制出来,再根据时间采用线性或者球形插值对动作序列的关键帧进行插值,不断变换顶点 坐标,从而得到了一系列连续的人物动画,听起来确实不难吧!当然你也可以在程序里自己 控制人物每一帧每一个关节的运动,我想做游戏的很少有人这么做吧。下面我向大家介绍一 下自己是如何编写人物动画程序的。本人从事的图形图象开发主要是基于 OpenGL 和 OSG 因此范例程序将采用 OpenGL 或 OSG。先声明一下,本人的语言表达能力很差,请大家多 多谅解指正。 考虑到没有接触过人物模型的朋友,我首先从人物模型的结构讲起,游戏人物编程主要 采用的人物模型格式有 Quake 里的md2,md3,Half Life 里的 mdl,Doom里的 md5,还有 典型的骨骼动画模型 ms3d…,至于3dmax 的模型,本人觉得太麻烦!在此我说两个有代表 性的 Md3,和 ms3d,其它的模型都大同小异,只要你了解了它们的格式,程序实现都不难。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值