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库中基础部分的介绍,后续还会记录在实际操作中如何使用这些类来完成一个完整的动画。








  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值