-
简介
在前文osgAnimation之动画基础篇中介绍了osgAnimation中的几个基础的类元素,这其中Channel频道类集成了关键帧、插值算法、采样器、执行对象(Target),构成了动画的基本要素。如何将这些要素应用到场景动画之中呢,我们查看osgAnimation::Channel,发现它里面有这样两个成员函数:
const std::string& getTargetName() const;
void setTargetName(const std::string& name);
设置作用对象的名称,这个作用对象是什么?它又能干什么呢?
首先我们想将关键帧插值的结果用到场景中,让场景不断地进行更新,首先我们想到的是节点的更新回调。更新回调会在每一帧中不断地被调用,通过将我们计算得到的插值应用到节点中(比如MatrixTransform)不断修改节点的位置、姿态或者纹理材质,从而实现动画的效果。这个作用对象就承载了将插值得到的结果应用到场景节点中的重任,真可谓是万事俱备只欠东风,有了作用对象这个通道,一切就变得水到渠成了。
-
作用对象一览
首先我们看看作用对象的继承图
Fig1. 作用对象继承图
可以看到通过继承自NodeCallback的作用对象可以实现位置的变化(如骨骼动画、加速减速等)而通过继承StateAttributeCallback则实现的动画更丰富(OpenGL中的状态,如颜色、材质、光照、雾效等等),下面我们分别就两种类型的动画作一些说明。
-
UpdateMatrixTransform
通过上面的继承图可以很清楚的看到这个类继承自NodeCallback,从它实现中我们看看它到底要做什么,既然是NodeCallback,那么就必然要重载operator() 操作
void UpdateMatrixTransform::operator()(osg::Node* node, osg::NodeVisitor* nv)
{
if (nv && nv->getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR)
{
osg::MatrixTransform* matrixTransform = dynamic_cast<osg::MatrixTransform*>(node);
if (matrixTransform)
{
// here we would prefer to have a flag inside transform stack in order to avoid update and a dirty state in matrixTransform if it's not require.
_transforms.update();
const osg::Matrix& matrix = _transforms.getMatrix();
matrixTransform->setMatrix(matrix);
}
}
traverse(node,nv);
}
看到了吧,
它就是用来在更新中修改MatrixTransform节点的矩阵从而达到每一帧变换位置实现动画效果的(也就是这个类必须设置为MatrixTransform的更新回调,不能设置为其他类型节点的更新回调)。这里还有一个需要我们了解的地方就是成员变量_transforms,这个变量实际上是一个类似于std::vector的数组,它的定义如下:
class StackedTransform : public osg::MixinVector<osg::ref_ptr<StackedTransformElement> >
{
public:
StackedTransform();
StackedTransform(const StackedTransform&, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY);
void update(float t = 0.0);
const osg::Matrix& getMatrix() const;
protected:
osg::Matrix _matrix;
};
osg::MixinVector重新实现了std::vector的所有接口,并且修正了std::vector可能出现的Bug,我们权当它是一个std::vector即可。我们在operator()中调用了StackedTransform的update方法:
void StackedTransform::update(float t)
{
int dirty = 0;
for (StackedTransform::iterator it = begin(); it != end(); ++it)
{
StackedTransformElement* element = it->get();
if (!element)
continue;
// update and check if there are changes
element->update(t);
if (element->isIdentity())
continue;
dirty++;
}
if (!dirty)
return;
// dirty update matrix
_matrix.makeIdentity();
for (StackedTransform::iterator it = begin(); it != end(); ++it)
{
StackedTransformElement* element = it->get();
if (!element || element->isIdentity())
continue;
element->applyToMatrix(_matrix);
}
}
实际上它只是依次调用每个存储在数组中元素(StackedTransformElement)的update方法,并最后将每个元素update调用之后的矩阵结果搜集到一起了。那么这个StackedTransformElement究竟是什么呢,我们接着看看它的继承关系:
Fig2 StackedTransformElement继承关系图
从继承类图上类的名字就很容易知道了,原来在StackedTransform数组里面存着这些类父类的指针,这些指针代表着子类的变换操作,如: 平移(StackedTranslateElement)、旋转(StackedQuaternionElement)、缩放(StackedScaleElement)等
这个类StackedTransformElement是一个虚类,它的实现方式都在子类中具现了,以平移的StackedTranslateElement为例:
void StackedTranslateElement::update(float /*t*/)
{
if (_target.valid())
_translate = _target->getValue();
}
可以看到它的update实现仅仅是获取到了_target中存储的值,通过查看源码可以发现所有StackedTransformElement派生类的update实现都只是简单地获取它们成员变量_target的值。这样我们就把
UpdateMatrixTransform::operator()
函数解释清楚了。
另一个需要解释的是UpdateMatrixTransform中的link函数,实现如下:
bool UpdateMatrixTransform::link(osgAnimation::Channel* channel)
{
const std::string& channelName = channel->getName();
// check if we can link a StackedTransformElement to the current Channel
for (StackedTransform::iterator it = _transforms.begin(); it != _transforms.end(); ++it)
{
StackedTransformElement* element = it->get();
if (element && !element->getName().empty() && channelName == element->getName())
{
Target* target = element->getOrCreateTarget();
if (target && channel->setTarget(target))
return true;
}
}
OSG_INFO << "UpdateMatrixTransform::link Channel " << channel->getName() << " does not contain a symbolic name that can be linked to a StackedTransformElement." << std::endl;
return false;
}
可以看到Channel中的Channel的名称必须与StackedTransformElement中某一元素的名称一致才能绑定成功,也就是将元素的Target设置为Channel的Target,让Target作为它们共同的媒介来传递数据。
现在我们动手写一个例子,用到了上面涉及到的所有类:
#include <osg/MatrixTransform>
#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
#include <osg/ShapeDrawable>
#include <osg/Shape>
#include <osgGA/TrackballManipulator>
#include <osgAnimation/Animation>
#include <osgAnimation/AnimationUpdateCallback>
#include <osgAnimation/UpdateMatrixTransform>
#include <osgAnimation/StackedTranslateElement>
#include <osgAnimation/StackedRotateAxisElement>
class PlayCallback : public osg::NodeCallback
{
public:
PlayCallback(osgAnimation::Animation *anim) : _animation(anim){ }
void operator() (osg::Node* node, osg::NodeVisitor* nv)
{
if (nv->getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR)
{
osgAnimation::ChannelList& channels = _animation->getChannels();
for (osgAnimation::ChannelList::iterator iter = channels.begin(); iter != channels.end(); ++iter)
{
osgAnimation::Target *target = (*iter)->getTarget();
target->reset();
}
const osg::FrameStamp *fs = nv->getFrameStamp();
_animation->update(fs->getSimulationTime());
}
traverse(node, nv);
}
protected:
osgAnimation::Animation* _animation;
};
int main()
{
osg::Geode *node = new osg::Geode;
node->addDrawable(new osg::ShapeDrawable(new osg::Sphere(osg::Vec3(0, 0, 0), 1)));
//创建动画频道并插入关键帧
osgAnimation::Vec3LinearChannel *ch1 = new osgAnimation::Vec3LinearChannel;
ch1->setName("position");
ch1->setTargetName("PathCallback");
ch1->getOrCreateSampler()->getOrCreateKeyframeContainer()->push_back(osgAnimation::Vec3Keyframe(0, osg::Vec3(0,0,0)));
ch1->getOrCreateSampler()->getOrCreateKeyframeContainer()->push_back(osgAnimation::Vec3Keyframe(1, osg::Vec3(10,0,0)));
//设置动画,注意weight必须设置大于0,因为在源码中会判断weight是否大于0
osgAnimation::Animation *anim = new osgAnimation::Animation;
anim->setPlayMode(osgAnimation::Animation::PPONG);
anim->setStartTime(0.0);
anim->addChannel(ch1);
anim->setWeight(1.0f);
//设置作用对象,注意我们频道的名称必须与作用对象数组里面的元素名称一致
osgAnimation::UpdateMatrixTransform *umt = new osgAnimation::UpdateMatrixTransform;
umt->setName("PathCallback");
umt->getStackedTransforms().push_back(new osgAnimation::StackedTranslateElement("position"));
//如果这里要链接Animation,那么必须保证utm->setName设置的和ch1的setTargetName完全一致
//umt->AnimationUpdateCallback::link(anim);
//如果直接链接channel就不需要设置两个name一致
umt->link(ch1);
//作用对象UpdateMatrixTransform必须设置为MatrixTransform的UpdateCallback
//设置为其他类型节点(包括PostionAttitudeTransform)都会失败
osg::MatrixTransform *mt = new osg::MatrixTransform;
mt->addChild(node);
mt->addUpdateCallback(umt);
osg::Group *root = new osg::Group;
root->addChild(mt);
root->setUpdateCallback(new PlayCallback(anim));
osgViewer::Viewer viewer;
viewer.setSceneData(root);
osgGA::TrackballManipulator *trackBall = new osgGA::TrackballManipulator;
trackBall->setHomePosition(osg::Vec3(0, -50, 0), osg::Vec3(0, 0, 0), osg::Y_AXIS);
viewer.setCameraManipulator(trackBall);
return viewer.run();
}
-
UpdateMorph
这个类实际上起到了变形动画的作用,通常也称之为逐顶点动画,是另外一种常见的三维动画表现形式。它记录了一系列顶点位置的坐标和偏移,并在动画运行的每一帧将各个顶点移动到新的位置,形成持续、平滑的运动效果。
该类的定义在osgAnimation/MorphGeometry和osgAnimation/MorphGeometry.cpp之中,与之对应的是MorphGeometry这个类,它派生自几何体对象osg::Geometry,我们看看它的实现,其中起变形的一段代码如下:
struct UpdateVertex : public osg::Drawable::UpdateCallback
{
virtual void update(osg::NodeVisitor*, osg::Drawable* drw)
{
MorphGeometry* geom = dynamic_cast<MorphGeometry*>(drw);
if (!geom)
return;
geom->transformSoftwareMethod();
}
};
继承了Drawable的UpdateCallback函数,在每一帧的变化中修改顶点,在transformSoftwareMethod函数中实现了顶点的计算。这个函数的实现有点长,仅摘选部分
// base * 1 - (sum of weights) + sum of (weight * target)
float baseWeight = 0;
for (unsigned int i=0; i < _morphTargets.size(); i++)
{
baseWeight += _morphTargets[i].getWeight();
}
baseWeight = 1 - baseWeight;
if (baseWeight != 0)
{
initialized = true;
for (unsigned int i=0; i < pos->size(); i++)
{
(*pos)[i] = _positionSource[i] * baseWeight;
}
if (_morphNormals)
{
for (unsigned int i=0; i < normal->size(); i++)
{
(*normal)[i] = _normalSource[i] * baseWeight;
}
}
}
可以看到新的顶点位置是根据所有添加到列表中的Geometry和它们相应的权值来进行计算的。
也就是这样一种情形:先定义一个原始的形状,然后在MorphGeometry内部的数组中添加很多几何体(通过addMorphTarget函数添加),每个几何体对应有一个权重,最后绘制出的形状是怎么样的是根据所有添加的几何体和它们的权重综合计算得到的。
接下来我们看看UpdateMorph的实现:与前面UpdateMatrixTransform一样,它也是继承自NodeCallback,那么必然要重载operator()函数:
void UpdateMorph::operator()(osg::Node* node, osg::NodeVisitor* nv)
{
if (nv && nv->getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR)
{
osg::Geode* geode = dynamic_cast<osg::Geode*>(node);
if (geode)
{
unsigned int numDrawables = geode->getNumDrawables();
for (unsigned int i = 0; i != numDrawables; ++i)
{
osgAnimation::MorphGeometry* morph = dynamic_cast<osgAnimation::MorphGeometry*>(geode->getDrawable(i));
if (morph)
{
// Update morph weights
std::map<int, osg::ref_ptr<osgAnimation::FloatTarget> >::iterator iter = _weightTargets.begin();
while (iter != _weightTargets.end())
{
if (iter->second->getValue() >= 0)
{
morph->setWeight(iter->first, iter->second->getValue());
}
++iter;
}
}
}
}
}
traverse(node,nv);
}
从它是实现我们可以知道UpdateMorph
必须设置为Geode的更新回调(和UpdateMatrixTransform不同),并且这个Geode节点下面必须有MorphGeometry节点的Drawable。UpdateMorph的作用是将MorphGeometry中的权值更新一遍,赶在更新回调进入MorphGeometry的Drawable::UpdateCallback之前更新它列表中Geometry的权值,使得后面Drawable更新回调中可以用到新的权值计算出新的顶点位置。
UpdateMorph另一个值得注意的地方是link函数的实现:
bool UpdateMorph::link(osgAnimation::Channel* channel)
{
// Typically morph geometries only have the weights for morph targets animated
std::istringstream iss(channel->getName());
int weightIndex;
iss >> weightIndex;
if (iss.fail())
{
return false;
}
if (weightIndex >= 0)
{
osgAnimation::FloatTarget* ft = _weightTargets[weightIndex].get();
if (!ft)
{
ft = new osgAnimation::FloatTarget;
_weightTargets[weightIndex] = ft;
}
return channel->setTarget(ft);
}
else
{
OSG_WARN << "Channel " << channel->getName() << " does not contain a valid symbolic name for this class" << std::endl;
}
return false;
}
在link中当链接频道时,需要频道的名称来创建一个索引值,也就是说我们的频道名称必须设置为"0", "1", "2" 这样的整数字符串,设置的频道名称和它们在MorphGeometry中添加进去的顺序是一致的。这和UpdateMatrixTransform有些类似,在UpdateMatrixTransform中的元素(StackedTransform)的名称要与Channel的一样。
关于UpdateMorph的例子参考osganimationmorph示例
-
UpdateMaterial
UpdateMaterial可以动态的更改物体的材质,它继承于osg::StateAttributeCallback,我们看一下它的实现:
void UpdateMaterial::operator()(osg::StateAttribute* sa, osg::NodeVisitor* nv)
{
if (nv && nv->getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR)
{
osg::Material* material = dynamic_cast<osg::Material*>(sa);
if (material)
update(*material);
}
}
可以看到对于UpdateMaterial,我们必须将它设置给Material对象的更新回调,update的实现如下:
osgAnimation::Vec4Target* UpdateMaterial::getDiffuse() { return _diffuse.get(); }
void UpdateMaterial::update(osg::Material& material)
{
osg::Vec4 diffuse = _diffuse->getValue();
material.setDiffuse(osg::Material::FRONT_AND_BACK, diffuse);
}
非常简单,就是设置材质而已。下面还需要看一下在link频道时有什么特别之处:
bool UpdateMaterial::link(osgAnimation::Channel* channel)
{
if (channel->getName().find("diffuse") != std::string::npos)
{
return channel->setTarget(_diffuse.get());
}
else
{
OSG_WARN << "Channel " << channel->getName() << " does not contain a valid symbolic name for this class " << className() << std::endl;
}
return false;
}
看到了吧,频道的名称中必须包含"diffuse"字样才可以正确链接成功,整个实现过程非常精简,下面我们使用一下该类:
#include <osg/Geode>
#include <osgViewer/Viewer>
#include <osgViewer/ViewerEventHandlers>
#include <osgGA/TrackballManipulator>
#include <osgGA/StateSetManipulator>
#include <osg/ShapeDrawable>
#include <osgAnimation/UpdateMaterial>
#include <osgAnimation/BasicAnimationManager>
#include <osgDB/ReadFile>
int main (int argc, char* argv[])
{
osgViewer::Viewer viewer;
osg::Geode *boxGeode = new osg::Geode;
boxGeode->addDrawable(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0, 0, 0), 2)));
osgAnimation::Animation* animation = new osgAnimation::Animation;
osgAnimation::Vec4LinearChannel * channel0 = new osgAnimation::Vec4LinearChannel;
channel0->getOrCreateSampler()->getOrCreateKeyframeContainer()->push_back(osgAnimation::Vec4Keyframe(0,osg::Vec4(1.0, 0.0, 0.0, 1.0)));
channel0->getOrCreateSampler()->getOrCreateKeyframeContainer()->push_back(osgAnimation::Vec4Keyframe(4,osg::Vec4(0.0, 0.0, 1.0, 1.0)));
//设置作用对象名称,必须和UpdateMaterial的名称设置为一样
channel0->setTargetName("UpdateMaterialCallback");
//频道的名称中必须包含"diffuse"字符
channel0->setName("diffuseChannel");
animation->addChannel(channel0);
animation->setPlayMode(osgAnimation::Animation::PPONG);
osgAnimation::BasicAnimationManager* bam = new osgAnimation::BasicAnimationManager;
bam->registerAnimation(animation);
osgAnimation::UpdateMaterial* updateMaterial = new osgAnimation::UpdateMaterial("UpdateMaterialCallback");
osg::Material *boxMaterial = new osg::Material;
//添加UpdateMaterial到Material对象的更新回调之中
boxMaterial->setUpdateCallback(updateMaterial);
boxMaterial->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(0, 0, 0, 1));
boxGeode->getOrCreateStateSet()->setAttribute(boxMaterial, osg::StateAttribute::ON);
viewer.setCameraManipulator(new osgGA::TrackballManipulator());
osg::Group* scene = new osg::Group;
scene->addUpdateCallback(bam);
scene->addChild(boxGeode);
viewer.addEventHandler(new osgViewer::StatsHandler());
viewer.addEventHandler(new osgViewer::WindowSizeHandler());
viewer.addEventHandler(new osgGA::StateSetManipulator(viewer.getCamera()->getOrCreateStateSet()));
viewer.setSceneData( scene );
viewer.realize();
bam->playAnimation(animation);
while (!viewer.done())
{
viewer.frame();
}
return 0;
}
运行之后会看到一个立方体不断地修改材质,这段代码和UpdateMatrixTransform以及UpdateMorph很多相似之处,只是更新回调设置的位置不同、频道的名称设置不同而已。
-
总结
从以上的整个分析来看,作用对象AnimationUpdateCallback的原理十分类似,使用过程也基本有一个相似的流程。三个的共同之处:
1.都需要配合动画管理器,动画管理器都要设置在父节点之上;
2.频道的名称都有一些需要注意的设置
不同之处:
1.三者设置为不同类型节点的更新回调
2.对应的频道中的关键帧类型不一样(UpdateMatrixTransform最灵活、UpdateMorph是float类型的,UpdateMaterial是osg::Vec4类型的)