第36.3节 动画-路径动画生成要点

Source

本功功能

本节我们在场景中创建了3个关键点,连成一条线,一个小人会从第一个点,跑到最后一个点,如图所示:
在这里插入图片描述
本节的内容在网盘中,目录为/osgChina站长文集/文件中的附件/, 有附件的会根据节的编号存放在该目录:

请使用浏览器打开,平时遇到问题或加群也可以加我微信:13324598743:
链接:https://pan.baidu.com/s/13gwJLwo_LbRnN3Bl2NXXXw
提取码:xrf5

实现要点

路径生成

OSG用类osg::AnimationPath,其有一个方法叫做insert:
animationPath->insert(t, osg::AnimationPath::ControlPoint(keyPoint->at(i), rotate));

  • 第一个参数 t 代表路径经过当前点的时间
  • 第二个参数ControlPoint包含两个量,一个是位置一个是朝向

这两个参数合在一起就是在某个时间,经过了某个点朝向某个方向。时间很好求,两个关键点的距离除以速度就可以了,经们在此实现了一个函数,叫做:
osg::AnimationPath* CreateAnimate(osg::Vec3Array* keyPoint, float speed)

它的入参osg::Vec3Array,里面放的是一系列的点,speed代表这条路径的速度。

现在要注意两个问题,一个问题是朝向怎么计算,当前场景ceep.ive是沿着XY平面展开的,所以朝向的计算主要是沿着Z轴转动的角度。默认情况下我们是头顶向Z轴,朝向Y轴正方向的,所以我们计算这个角度用了一个简单的函数:
Quqt.makeRotate(osg::Y_AXIS, keyPoint->at(i+1) - keyPoint->at(i));

因为我们知道keyPoint中的点是沿XY平面展开的,所以它的向量与Y轴的夹角就是其旋转的角度。

第二个问题是朝向的插值
在这里插入图片描述
如图所示,蓝色的是压入的关键点和朝向,我们一看是合理的,在起点朝向第二个点,第二个点朝向第三个点,但是其中间的值就未必合理了,对于位置来说是插值形成的,从第一个点到第二个点中间的位置跟据时间插值来取,但是朝向也是的,如其中黄色的箭 头,它从起点的朝向,在两点之间平均的过渡到了第二个点的朝向,这个效果未必是我们想要的,也许我们想要他快跑到跟前的时候再转向。这就需要在计算路径的时候人为在转向的地方插值一个关键点。这也是我们经常做的操作。

人物加载

这个人物的动画控制是在第36.2节 动画-骨骼动画的控制,我们取其中第22个动作进行播放就得到了跑动的人,但是人的默认朝向并不是在头朝Z,看向Y,而且也太大了,我们就对其进行修改,先缩放个0.02倍,再沿X轴转90度,转完后再沿Z轴转180度,再向Z轴正方向抬高4米,才把这个人物放在地面上,而且头朝Z轴,看向Y轴的正方向,就保证了他沿着路径是在奔跑,而不是在倒着跑,头朝下跑或横着跑。

以上就是所有的关键点。

以下是所有代码

#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
#include <osg/NodeVisitor>
#include <osg/Transform>
#include <osg/AnimationPath>
#include <osgGA/GUIEventHandler>
#include <osgUtil/LineSegmentIntersector>
#include <osg/ShapeDrawable>
#include <osg/LineWidth>
#include <osgAnimation/BasicAnimationManager>
#include <osg/MatrixTransform>


//给定顶点,和行进的速度,创建一个路径
osg::AnimationPath* CreateAnimate(osg::Vec3Array* keyPoint, float speed)
{
    
      
    //只有1个点,是形成不了路径的
    if (keyPoint->size() <= 1)
    {
    
      
        return NULL;
    }

    osg::AnimationPath* animationPath = new osg::AnimationPath;
    animationPath->setLoopMode(osg::AnimationPath::LOOP);

    //关键点对应的时间
    float t = 0.0;
    //朝向
    osg::Quat rotate;
    for (int i = 0; i < keyPoint->size(); i++)
    {
    
      
        //最后一个点
        if ((keyPoint->size()-1) == i)
        {
    
      
            //朝向不计算了,就用上一个点的朝向
            animationPath->insert(t, osg::AnimationPath::ControlPoint(keyPoint->at(i), rotate));
            //循环结束
            break;
        }
        else
        {
    
      
            //其它点
            //如果是第一个点,时间上是t=0.0, 朝向上由第一个点和第二个点的朝向决定
            //计算当前点的朝向,因为我们的朝向只在一个轴上水平的变化,所以可以用简单的方法
            //视口默认朝Y轴,我们要将其转到由第一个点看向第二个点的向量
            rotate.makeRotate(osg::Y_AXIS, keyPoint->at(i+1) - keyPoint->at(i));
            animationPath->insert(t, osg::AnimationPath::ControlPoint(keyPoint->at(i), rotate));
        }

        //两点间的距离除以速度
        t += ((keyPoint->at(i+1) - keyPoint->at(i)).length()/speed);

    }

    return animationPath;
}

//创建一个红球做标志,给定圆心和半径
osg::Node* CreateSphere(osg::Vec3 center, float r)
{
    
      
    osg::Geode* gnode = new osg::Geode;
    osg::ShapeDrawable* sd = new osg::ShapeDrawable(new osg::Sphere(center, r));
    sd->setColor(osg::Vec4(1.0, 0.0, 0.0, 1.0));//红色
    gnode->addDrawable(sd);
    return gnode;
}

//创建线,给定顶点和红宽,
osg::Node* CreateLine(osg::Vec3Array* vertex, float lineWidth)
{
    
      
    osg::Geode* gnode = new osg::Geode;
    osg::Geometry* geom = new osg::Geometry;
    gnode->addDrawable(geom);
    
    //设置顶点
    geom->setVertexArray(vertex);

    //设置颜色
    osg::Vec4Array* color = new osg::Vec4Array();
    color->push_back(osg::Vec4(1.0, 0.0, 0.0, 1.0));
    geom->setColorArray(color, osg::Array::BIND_OVERALL);

    //设置线宽
    osg::LineWidth* lw = new osg::LineWidth(lineWidth);
    geom->getOrCreateStateSet()->setAttribute(lw, osg::StateAttribute::ON | osg::StateAttribute::PROTECTED);
    //关闭灯光显得亮一点
    geom->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);

    //设置添加为线
    geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINE_STRIP, 0, vertex->size()));

    return gnode;
}

osg::Group* createMark(osg::Vec3Array* keyPoint)
{
    
      
    osg::Group* markGroup = new osg::Group();

    //添加红球做关键点
    for (int i = 0; i < keyPoint->size(); i++)
    {
    
      
        markGroup->addChild(CreateSphere(keyPoint->at(i), 1.0));
    }

    //添加线
    markGroup->addChild(CreateLine(keyPoint, 1.0));

    return markGroup;
}

//添加运动的小人,沿着animation运动
osg::Node* loadPersion(osg::AnimationPath* animation)
{
    
      
    //用于旋转小人,使其默认朝向正确
    osg::MatrixTransform* mt = new osg::MatrixTransform;

    osg::Node* persion = osgDB::readNodeFile("Naruto.fbx");
    //因为建模的时候,小人的朝向是不定的,我们要看其加载到OSG的朝向,然后把它调整到朝向Y轴,头顶Z轴,
    //这是我们建立这个路径时候,计算夹角是以头顶Z轴,朝向Y轴为0度
    //先缩小0.02倍,再给小人沿x轴旋转90度,再给小人沿Z轴转180度,这样其朝向就是朝向Y轴正方向了,再沿Z轴正方向提4米
    mt->setMatrix(osg::Matrix::scale(0.02, 0.02, 0.02)*osg::Matrix::rotate(osg::inDegrees(90.0), osg::X_AXIS) * osg::Matrix::rotate(osg::inDegrees(180.0), osg::Z_AXIS)*osg::Matrix::translate(osg::Vec3(0.0, 0.0, 4.0)));
    mt->addChild(persion);

    //播放其第22个动作,是奔跑
    osgAnimation::BasicAnimationManager* apc = dynamic_cast<osgAnimation::BasicAnimationManager*>(persion->getUpdateCallback());
    apc->playAnimation(apc->getAnimationList().at(22));

    //实际的路径控制
    osg::MatrixTransform* persionMT = new osg::MatrixTransform;
    persionMT->setUpdateCallback(new osg::AnimationPathCallback(animation));
    persionMT->addChild(mt);

    return persionMT;
}


int main()
{
    
      
    osgViewer::Viewer viewer;
    //里面存放的是路径上的关键点
    osg::Vec3Array* keyPoint = new osg::Vec3Array;
    keyPoint->push_back(osg::Vec3(-28.22, -63.42, 1.0));
    keyPoint->push_back(osg::Vec3(3.38, -32, 1.0));
    keyPoint->push_back(osg::Vec3(40.34, -38.40, 1.0));

    osg::Node* ceep = osgDB::readNodeFile("ceep.ive");

    //场景ceep
    osg::Group* root = new osg::Group;
    root->addChild(ceep);
    //关键点和路径的标记
    root->addChild(createMark(keyPoint));

    //添加运动的小人
    root->addChild(loadPersion(CreateAnimate(keyPoint, 2.5)));

    viewer.setSceneData(root);

    return viewer.run();
}