OSG 使用整理(3):自定义漫游器动画

发布时间 2023-05-02 13:05:56作者: 王小于的啦

自定义漫游器动画

1 相机视图矩阵

1.1   坐标系统

 

(1)局部坐标系:以三维物体中的某个原点建立顶点比较方便,事实上一个复杂物体可能有多个局部坐标系,每个局部坐标系用于其某个部位。通过一组平移、旋转和缩放变换的组合,可以将局部坐标系变换到世界坐标系。

(2)世界坐标系:为了定义所有物体之间的空间关系,必须将这些物体变换到这个公共空间中。该变换由模型矩阵(Model Matrix)实现。

(3)摄像机/观察坐标系:观察坐标系就是从摄像机的视角所观察到的空间,而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常储存在一个观察矩阵(View Matrix)中。

(4)标准化设备坐标系:定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。

(5)屏幕坐标系:它的原点位于屏幕的坐上角,y 轴正向垂直向下。

在OpenGL中,本地坐标系、世界坐标系和观察坐标系都属于右手坐标系,而最终的裁剪坐标系和标准化设备坐标系属于左手坐标系。

1.2   视图矩阵(View Matrix)详细推导

观察坐标系中,场景中所有顶点坐标经过视图变换到了以摄像机为原点的观察坐标。要定义一个摄像机,需要指定它在世界坐标系上的位置、观察方向、向右和向上方向。将摄像机位置设为eye,朝向的点为lookAt,向上向量为up,创建一个以摄像机为原点的坐标系。

(1)   首先计算方向向量:

dir=(eye-lookAt).normalize()

(2) 然后根据向上向量和方向向量计算得到摄像机右向量:

right=cross(dir,up).normalize()

(3) 然后计算计算单位向上向量:

up=cross(dir,right).normalize()

我们将摄像机放到世界坐标系原点,朝向-z轴方向,向上方向与y轴方向重合,这时候相机坐标系就和世界坐标系重合。这个过程分成两个矩阵,一是把摄像机移动到原点的平移变换(Transform Matrix),二是将摄像机的方向旋转变换(Rotate Matrix)到正确方向。

平移矩阵如下:

 

旋转逆矩阵中的各分量值即为x,y,z三个单位向量旋转后的值:

因为正交矩阵的逆等于转置,得到最终的视图矩阵:

1.1   OSG中视图矩阵

OSG提供osg::MatrixTransform类和osg::PositionAttitudeTransform子类改变节点的模型变换矩阵(Model Matrix)。另外提供了osg::Camera类计算相机视图矩阵(View Maatrix),但实际上相机的视图矩阵是由查看器的内部osgGA::CameraManipulator对象进行控制的。

(1) View::setCameraManipulator方法new 一个漫游器对象。

(2) View::init方法中调用漫游器初始化虚函数init

 1 void View::init()
 2 {
 3     OSG_INFO<<"View::init()"<<std::endl;
 4 
 5     osg::ref_ptr<osgGA::GUIEventAdapter> initEvent = _eventQueue->createEvent();
 6     initEvent->setEventType(osgGA::GUIEventAdapter::FRAME);
 7  if (_cameraManipulator.valid())
 8     {
 9         _cameraManipulator->init(*initEvent, *this);
10     }
11 }

(3) Viewer::eventTraversal方法事件遍历最后调用漫游器handle处理事件响应。

1 for(osgGA::EventQueue::Events::iterator itr = events.begin();itr != events.end();++itr)
2 {
3         osgGA::Event* event = itr->get();
4         if (event && _cameraManipulator.valid())
5         {
6             _cameraManipulator->handle( event, 0, _eventVisitor.get());
7         }
8 }

(4) Viewer::updateTraversal方法更新遍历中调用漫游器updateCamera更新相机视图矩阵。

1 /** update the camera for the current frame, typically called by the viewer classes.
2 Default implementation simply set the camera view matrix. */
3 virtual void updateCamera(osg::Camera& camera) { camera.setViewMatrix(getInverseMatrix()); }

2 OSG漫游器

2.1 CameraManipulator虚基类

       osgGA:: CameraManipulator虚基类继承osgGA:: GUIEventHandler,以处理事件消息,为子类提供接口:

 1 /** set the position of the matrix manipulator using a 4x4 Matrix.*/
 2 virtual void setByMatrix(const osg::Matrixd& matrix) = 0;
 3 
 4 /** set the position of the matrix manipulator using a 4x4 Matrix.*/
 5 virtual void setByInverseMatrix(const osg::Matrixd& matrix) = 0;
 6 
 7 /** get the position of the manipulator as 4x4 Matrix.*/
 8 virtual osg::Matrixd getMatrix() const = 0;
 9 
10 /** get the position of the manipulator as a inverse matrix of the manipulator, typically used as a model view matrix.*/
11 virtual osg::Matrixd getInverseMatrix() const = 0;
12 
13 /** Handle events, return true if handled, false otherwise. */
14 virtual bool handle(const GUIEventAdapter& ea,GUIActionAdapter& us);

2.2 StandardManipulator虚基类

  osgGA:: StandardManipulator虚基类继承osgGA:: CameraManipulator,补充了设置计算视图矩阵的接口和初始化方法:

 1 /** Sets manipulator by eye position and eye orientation.*/
 2 virtual void setTransformation( const osg::Vec3d& eye, const osg::Quat& rotation ) = 0;
 3 
 4 /** Sets manipulator by eye position, center of rotation, and up vector.*/
 5 virtual void setTransformation( const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up ) = 0;
 6 
 7 /** Gets manipulator's eye position and eye orientation.*/
 8 virtual void getTransformation( osg::Vec3d& eye, osg::Quat& rotation ) const = 0;
 9 
10 /** Gets manipulator's focal center, eye position, and up vector.*/
11 virtual void getTransformation( osg::Vec3d& eye, osg::Vec3d& center, osg::Vec3d& up ) const = 0;
12 
13 virtual void init( const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& us );

  并且重写了事件响应handle方法,大概有刷新事件、窗口大小重置事件、鼠标事件、键盘事件等类型。

 1 /** Handles events. Returns true if handled, false otherwise.*/
 2 bool StandardManipulator::handle( const GUIEventAdapter& ea, GUIActionAdapter& us )
 3 {
 4     switch( ea.getEventType() )
 5     {
 6 
 7         case GUIEventAdapter::FRAME:
 8             return handleFrame( ea, us );
 9 
10         case GUIEventAdapter::RESIZE:
11             return handleResize( ea, us );
12 
13         default:
14             break;
15    }
16 
17     if( ea.getHandled() )
18         return false;
19 
20     switch( ea.getEventType() )
21     {
22         case GUIEventAdapter::MOVE:
23             return handleMouseMove( ea, us );
24 
25         case GUIEventAdapter::DRAG:
26             return handleMouseDrag( ea, us );
27 
28         case GUIEventAdapter::PUSH:
29             return handleMousePush( ea, us );
30 
31         case GUIEventAdapter::RELEASE:
32             return handleMouseRelease( ea, us );
33 
34         case GUIEventAdapter::KEYDOWN:
35             return handleKeyDown( ea, us );
36 
37         case GUIEventAdapter::KEYUP:
38             return handleKeyUp( ea, us );
39 
40         case GUIEventAdapter::SCROLL:
41             if( _flags & PROCESS_MOUSE_WHEEL )
42             return handleMouseWheel( ea, us );
43             else
44             return false;
45 
46         default:
47             return false;
48     }
49 }

  事件处理函数内部只简单记录了当前时间和屏幕坐标,另外鼠标事件还调用了performMovement函数,细分了左中右键点击处理函数。

2.3 OrbitManipulator类

       osgGA:: OrbitManipulator类为轨道漫游器,可以使得相机围绕目标进行轨道运动,维护了目标中心(osg::Vec3d _center )旋转四元数(osg::Quat  _rotation)相机距离(double     _distance)三个关键数据,类似于1.2节中过程可以由此推导出视图矩阵。最终返回的矩阵如下:

 1 /** Get the position of the manipulator as a inverse matrix of the manipulator,
 2 typically used as a model view matrix.*/
 3 osg::Matrixd OrbitManipulator::getInverseMatrix() const
 4 {
 5     return osg::Matrixd::translate( -_center ) *
 6            osg::Matrixd::rotate( _rotation.inverse() ) *
 7            osg::Matrixd::translate( 0.0, 0.0, -_distance );
 8 }
 9 // doc in parent
10 void OrbitManipulator::setTransformation( const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up )
11 {
12     Vec3d lv( center - eye );
13 
14     Vec3d f( lv );
15     f.normalize();
16     Vec3d s( f^up );
17     s.normalize();
18     Vec3d u( s^f );
19     u.normalize();
20 
21     osg::Matrixd rotation_matrix( s[0], u[0], -f[0], 0.0f,
22                             s[1], u[1], -f[1], 0.0f,
23                             s[2], u[2], -f[2], 0.0f,
24                             0.0f, 0.0f,  0.0f, 1.0f );
25 
26     _center = center;
27     _distance = lv.length();
28     _rotation = rotation_matrix.getRotate().inverse();
29 
30     // fix current rotation
31     if( getVerticalAxisFixed() )
32         fixVerticalAxis( _center, _rotation, true );
33 }

  首先将漫游器移动到目标中心,然后旋转对齐方向向量,最后移动到观察位置。

  osgGA:: OrbitManipulator类重载了鼠标事件处理方法,详细编写了左中右键点击时间处理。其中左键拖动鼠标默认以轨道方式绕着目标旋转漫游器,中键拖动鼠标默认在xy平面平移漫游器,右键拖动鼠标默认在z轴移动漫游器,来达到缩放效果。

  综上分析,想要实现漫游器动画,需要重载函数修改目标中心、旋转四元数、相机距离这三个数据。

2.3 AnimationPathManipulator类

  osgGA:: AnimationPathManipulator类继承osgGA:: CameraManipulator,其内部维护了私有变量动画路径osg::AnimationPath _animationPath。初始化方法中可以直接导入动画路径,也可以外部设置,动画路径_animationPath需要设置动画循环模式、开始结束时间、控制点。osgGA:: AnimationPathManipulator类重载了事件响应方法:

 1 bool AnimationPathManipulator::handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter& us)
 2 {
 3     if( !valid() ) return false;
 4 
 5     switch( ea.getEventType() )
 6     {
 7     case GUIEventAdapter::FRAME:
 8         if( _isPaused )
 9         {
10             handleFrame( _pauseTime );
11         }
12         else
13         {
14             handleFrame( ea.getTime() );
15         }
16         return false;
17     case GUIEventAdapter::KEYDOWN:
18             if (ea.getKey()==' ')
19             {
20                 _isPaused = false;
21 
22                 home(ea,us);
23                 us.requestRedraw();
24                 us.requestContinuousUpdate(false);
25 
26                 return true;
27             }
28             else if (ea.getKey()==')')
29             {
30                 double time = _isPaused ? _pauseTime : ea.getTime();
31                 double animationTime = (time+_timeOffset)*_timeScale;
32 
33                 _timeScale *= 1.1;
34 
35                 OSG_NOTICE<<"Animation speed = "<<_timeScale*100<<"%"<<std::endl;
36 
37                 // adjust timeOffset so the current animationTime does change.
38                 _timeOffset = animationTime/_timeScale - time;
39 
40                 return true;
41             }
42             else if (ea.getKey()=='(')
43             {
44                 double time = _isPaused ? _pauseTime : ea.getTime();
45                 double animationTime = (time+_timeOffset)*_timeScale;
46 
47                 _timeScale /= 1.1;
48 
49                 OSG_NOTICE<<"Animation speed = "<<_timeScale*100<<"%"<<std::endl;
50 
51                 // adjust timeOffset so the current animationTime does change.
52                 _timeOffset = animationTime/_timeScale - time;
53 
54                 return true;
55             }
56             else if(ea.getKey() == 'p')
57             {
58                 if( _isPaused )
59                 {
60                     _isPaused = false;
61                     _timeOffset -= ea.getTime() - _pauseTime;
62                 }
63                 else
64                 {
65                     _isPaused = true;
66                     _pauseTime = ea.getTime();
67                 }
68                 us.requestRedraw();
69                 us.requestContinuousUpdate(false);
70                 return true;
71             }
72 
73         break;
74         default:
75             break;
76     }
77     return false;
78 }

  handle方法中通过handleFrame更新视图矩阵。

 1 void AnimationPathManipulator::handleFrame( double time )
 2 {
 3     osg::AnimationPath::ControlPoint cp;
 4 
 5     double animTime = (time+_timeOffset)*_timeScale;
 6     _animationPath->getInterpolatedControlPoint( animTime, cp );
 7 
 8     if (_numOfFramesSinceStartOfTimedPeriod==-1)
 9     {
10         _realStartOfTimedPeriod = time;
11         _animStartOfTimedPeriod = animTime;
12 
13     }
14 
15     ++_numOfFramesSinceStartOfTimedPeriod;
16 
17     double animDelta = (animTime-_animStartOfTimedPeriod);
18     if (animDelta>=_animationPath->getPeriod())
19     {
20         if (_animationCompletedCallback.valid())
21         {
22             _animationCompletedCallback->completed(this);
23         }
24 
25         if (_printOutTimingInfo)
26         {
27             double delta = time-_realStartOfTimedPeriod;
28             double frameRate = (double)_numOfFramesSinceStartOfTimedPeriod/delta;
29             OSG_NOTICE <<"AnimatonPath completed in "<<delta<<" seconds, completing "<<_numOfFramesSinceStartOfTimedPeriod<<" frames, average frame rate = "<<frameRate<<std::endl;
30         }
31 
32         // reset counters for next loop.
33         _realStartOfTimedPeriod = time;
34         _animStartOfTimedPeriod = animTime;
35         _numOfFramesSinceStartOfTimedPeriod = 0;
36     }
37 
38     cp.getMatrix( _matrix );
39 }

3 自定义OSG漫游器视角动画

3.1 四元数与三维旋转

       本篇论述引自Krasjet的博文re:https://krasjet.github.io/quaternion/quaternion.pdf,他从几何和计算机图形学角度上,介绍了四元数的概念和应用,详细讨论了四元数和三维旋转之间的关系。

(1)   定义:

(2)   四则运算:

  加减法各分量相加减

  标量乘法:乘以每一项

  四元数乘法: 

 

 

 

(3)   3D旋转公式 

(4)   设单位四元数  

则它对应的旋转角度、旋转轴为 

θ/2=(cos)^(-1) (a)
u=V/(sin⁡((cos)^(-1) (a)))

同一个3D旋转可以使用两个不同的四元数来表示,如果?表示的是沿着旋转轴u旋转θ度,那么−?代表的是沿着相反的旋转轴−u旋转(2? − θ)度。

(5)   slerp插值(球面线性插值,Spherical Linear Interpolation)

qt = Slerp(q0,q1,t) = sin((1 - t)θ) sin(θ) q0 + sin(tθ) sin(θ) q1

  其中q0、q1之间的夹角可以直接使用它们的点乘的结果:。注意到插值时检查夹角是否过小,导致除零的问题。

3.2 缓动函数

  re: https://easings.net/

  缓动函数代表的是一个数学函数,这个数学函数描述动画期间一维值变化的速度。这让你在整个动画过程中改变动画的速度。

 

  QT自带缓动函数类QEasingCurve,头文件qeasingcurve.h,使用如下:

1 QEasingCurve easing(QEasingCurve::InOutQuad);
2 
3 for (qreal t = 0.0; t < 1.0; t += 0.1)
4    qWarning() << "Effective progress" << t << "is"
5              << easing.valueForProgress(t);

3.3 自定义漫游器视角动画

       结合上述理论,不使用osgGA:: AnimationPathManipulator类,也可以实现自定义漫游器视角动画,关键点:

       (1)继承osgGA:: OrbitManipulator类,重载GUIEventAdapter::FRAME:里面的刷新事件,更新旋转四元数(osg::Quat  _rotation)。

       (2)创建最终旋转状态四元数,设置坐标轴和旋转角,使用slerp插值,得到中间状态插值的四元数。

       (3)使用QT自带缓动函数,优化动画效果。设置动画执行时间、开始结束时间、缓动函数类型。