第4节 操作器和Trackball

文章发布之后,大家有很多疑问,我发现有些很基础。要求我再聊细一些,我就再重头来再比较细致的写一写。虽然不需要大家看代码,但是可能在讲代码的时候大家还是希望能有对照,作者使用的是3.6.5版本,我已经上传到了百度网盘里。大家可以通过如下方式下载:

【击此打开网盘资源链接】

对于只有三行的最简单的osg程序:

    osgViewer::Viewer viewer;
    viewer.setSceneData(osgDB::readNodeFile("glider.osg"));
    return viewer.run();

我们重新回到Viewer::run()这个函数里,这个函数的调用是如下这样子的:

int Viewer::run()
{
    if (!getCameraManipulator() && getCamera()->getAllowEventFocus())
    {
        setCameraManipulator(new osgGA::TrackballManipulator());
    }

    setReleaseContextAtEndOfFrameHint(false);

    return ViewerBase::run();
}

从字面上我们很容易理解,首先判断当前viewer是不是设置了操作器,然后再看相机是不是允许获取焦点,假如不允许获取焦点,那鼠标键盘都点不上去,是不需要操作器的。

那么getCameraManipulator()我们知道我们没有调用过setCameraManipulator所以这个得到的是null。但是getCamera()不会返回空吗?答案是不会的,这个camera的申请阶段是在osg::View的构造函数里osgViewer::Viewer派生自osg::View因此自然构造。初始化的代码在osg/view.cpp的第27~40行,如下:

View::View():Object(true)
{
...
    _camera = new osg::Camera;
    _camera->setView(this);

    double height = osg::DisplaySettings::instance()->getScreenHeight();
    double width = osg::DisplaySettings::instance()->getScreenWidth();
    double distance = osg::DisplaySettings::instance()->getScreenDistance();
    double vfov = osg::RadiansToDegrees(atan2(height/2.0f,distance)*2.0);

    _camera->setProjectionMatrixAsPerspective( vfov, width/height, 1.0f,10000.0f);

    _camera->setClearColor(osg::Vec4f(0.2f, 0.2f, 0.4f, 1.0f));

    osg::StateSet* stateset = _camera->getOrCreateStateSet();
    stateset->setGlobalDefaults();
...
}

没有操作器的话就会调用setCameraManipulator(new osgGA::TrackballManipulator());设置Trackball了,那么操作器是如何在场景中起到拖拖拽拽起作用的呢?以及Trackball是怎么回事儿呢?怎么就场景上来就会在屏幕的中间,一拖就会走呢?下面来一一解答。

操作器怎么起作用的?

所有的操作器都派生自osgGA::CameraManipulator,而这个CameraManipulator又派生自osgGA::GUIEventHandler,可见其本质上是个事件处理的类。因此它首先会接收事件,比如鼠标一拖,场景就动。场景动与不动是受视点的位置、朝向来决定的,也就是观察矩阵,因此CameraManipulator必有处理事件的接口和输出矩阵的接口,处理事件的接口是函数bool handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter &aa);所以每个操作器必须要对这个函数大书特书来达到按自己的操作来达成接口的目的。这个handle的被调用的地方是在一帧绘制的事件遍历阶段,是在void Viewer::eventTraversal()中,viewer.cpp的第1126行:_cameraManipulator->handle( event, 0, _eventVisitor.get());虽然入参不一样,但是跟踪一下会发现会调用过去的。

事件处理之后,就要根据处理的事件改变视点的位置来达到操作场景的目的。这个是通过osgGA::CameraManipulator的函数virtual osg::Matrixd getInverseMatrix()来实现的,它的调用时机是在更新阶段void Viewer::updateTraversal()中,第1213行:_cameraManipulator->updateCamera(*_camera);它会调用至:camera.setViewMatrix(getInverseMatrix());这个时候就有人不明白了,为什么要使用逆矩阵呢?原因这里有点绕,大家认真理解一下,世界坐标下,一个盒子在A点(xa,ya,za),视点在B点(xb,yb,zb),先不管朝向不朝向的。我们要输出此时在B点的人看到A点的图像,做法有很多,实际上图形学界是这么做的,把A点先转换为以B点为局部坐标的坐标系下,然后再进行一系列操作。这个变换的过程其实相当于需要求出把B点移回世界坐标原点的矩阵,A点再乘以这个矩阵就可以,这个移回的矩阵是这样计算的,把人从世界坐标原点移到B点的变换是Matrix,而把B点移回世界坐标原点的变换就是InverseMatrix,现在我们要把A点放在以B点为局部坐标下,则需要变换的就是InverseMatrix。有点绕,但是明白这一点还是很重要的,属于基本功。

Trackball是如何上来就看着物体的
Trackball的第一个特点是上来就会看着场景的物体,这是怎么实现的呢?首先setCameraManipulator其实有两个参数:void setCameraManipulator(osgGA::CameraManipulator* manipulator, bool resetPosition = true);第二个参数默认是true,意思就是初始时要设置到home的位置,其实是调用home函数,看看这个函数的代码:

void View::setCameraManipulator(osgGA::CameraManipulator* manipulator, bool resetPosition)
{
    _cameraManipulator = manipulator;

    if (_cameraManipulator.valid())
    {
        _cameraManipulator->setCoordinateFrameCallback(new ViewerCoordinateFrameCallback(this));

        if (getSceneData()) _cameraManipulator->setNode(getSceneData());

        if (resetPosition)
        {
            osg::ref_ptr<osgGA::GUIEventAdapter> dummyEvent = _eventQueue->createEvent();
            _cameraManipulator->home(*dummyEvent, *this);
        }
    }
}

而home是如何定位到正好看见物体的呢?不大也不小,不近也不远的。在home这个函数中,它调用了virtual void computeHomePosition(const osg::Camera *camera = NULL, bool useBoundingBox = false);来计算了一下home的位置,其计算方法也很一目了然,先计算包围盒,然后再调用setHomePosition根据包围盒的中心点和半径来设置视点的位置和朝向。virtual void setHomePosition(const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up, bool autoComputeHomePosition=false)

Trackball是如何实现鼠标惯性操作的?
Trackball有两个特征:
1、鼠标按下,拖动,停下,鼠标弹起。则场景转动,在鼠标弹起时不转了。
2、鼠标按下,甩出来,则场景一直转动。

这两个的差别关键在鼠标的弹起操作,一个是停下弹,一个是甩弹。这个是在这里判断的,bool StandardManipulator::handleMouseRelease( const GUIEventAdapter& ea, GUIActionAdapter& us )是操作器的鼠标弹起事件:这里的关键是鼠标弹起的时候和上一个鼠标移动的时候的时间差>0.02秒,则认为是情况1,小于等于0.02秒认为是甩。

bool StandardManipulator::handleMouseRelease( const GUIEventAdapter& ea, GUIActionAdapter& us )
{
    //判断是DRAG,而不是MOVE,DRAG是有键按下了,Mask=0
    if( ea.getButtonMask() == 0 )
    {

        double timeSinceLastRecordEvent = _ga_t0.valid() ? (ea.getTime() - _ga_t0->getTime()) : DBL_MAX;
        //判断上一个事件和当前事件的时间差,小于0.02秒可认为是甩
        if( timeSinceLastRecordEvent > 0.02 )
            flushMouseEventStack();

        if( isMouseMoving() )
        {

            if( performMovement() && _allowThrow )
            {
                us.requestRedraw();
                us.requestContinuousUpdate( true );
                _thrown = true;
            }

            return true;
        }
    }

    flushMouseEventStack();
    addMouseEvent( ea );
    if( performMovement() )
        us.requestRedraw();
    us.requestContinuousUpdate( false );
    _thrown = false;

    return true;
}

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片