osgAnimation是osg库中提供场景动画效果的一个类库,它为我们提供了许多与场景动画相关的类,比如关键帧、插值、采样、频道、骨骼动画、材质变化等。本课就对osgAnimation库中的基础类进行一些解析。以下都是我个人学习过程中的一些记录和体会,方便以后自己复习之用。
对应文件 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这样的方法往容器里面插入关键帧。
对应文件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(贝塞尔插值)
对应文件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(动画频道的概念)
对应文件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对象之中 }
对应文件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库中基础部分的介绍,后续还会记录在实际操作中如何使用这些类来完成一个完整的动画。