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 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 TemplateKeyframeContainer : public std::vector >, public KeyframeContainer
{
public:
TemplateKeyframeContainer() {}
typedef TemplateKeyframe KeyType;
virtual unsigned int size() const { return (unsigned int)std::vector >::size(); }
};
可以看到该容器继承自std::vector,这样我们就可以采用push_back这样的方法往容器里面插入关键帧。
对应文件osgAnimation/Interpolator
有了关键帧之后,我们需要对关键帧之间的时间对应的值进行计算,这就是所谓的插值,定义插值的基类如下:
template
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& 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* 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 TemplateStepInterpolator : public TemplateInterpolatorBase
{
public:
TemplateStepInterpolator() {}
void getValue(const TemplateKeyframeContainer& 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 TemplateSampler : public Sampler
{
public:
//KeyframeType关键帧的类型
typedef typename F::KeyframeType KeyframeType;
//关键帧容器类型
typedef TemplateKeyframeContainer 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 _keyframes;
};
到这里我们已经可以将采样器应用到我们的程序中了,例如:自己定义更新回调,传入关键帧参数,根据关键帧计算每个时刻的值(比如物体姿态),并进行更新来达到动画效果。在osgAnimation中还进行了更高的封装,即Channel(动画频道的概念)
对应文件osgAnimation/Channel和osgAnimation/Channel.cpp
在一个Channel之中封装了采样器Sampler和执行对象Target,执行对象可以理解为将采样器计算的插值结果保存在这个对象之中,查看一下Target的实现如下:
template
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库中基础部分的介绍,后续还会记录在实际操作中如何使用这些类来完成一个完整的动画。