查看: 5221|回复: 7

开贴讨论适用于osg的heading,pitch,roll

[复制链接]

该用户从未签到

发表于 2013-1-8 20:24:23 | 显示全部楼层 |阅读模式
本帖最后由 woshijiameizhou 于 2013-1-8 20:43 编辑

      一直以来每当遇到heading,pitch,roll,总显得中气不足,今天大胆开贴讨论一下这个问题,以下仅是学习过程中的心得,对错无从考证,仅供参考,欢迎讨论,欢迎拍砖。
      
     osg中缺省的观察矩阵是什么,看osgGA :: CameraManipulator的构造函数
  1. CameraManipulator::CameraManipulator()
  2. {
  3.     _intersectTraversalMask = 0xffffffff;

  4.     _autoComputeHomePosition = true;

  5.     _homeEye.set(0.0,-1.0,0.0);
  6.     _homeCenter.set(0.0,0.0,0.0);
  7.     _homeUp.set(0.0,0.0,1.0);
  8. }
复制代码
camera中有一个方法setViewMatrixAsLookAt()理解起来比较直观,需要传递三个参数:eye,center,up。
       CameraManipulator构造函数中最后三行代码说明了osg中缺省的观察矩阵即为:观察者eye在世界坐标系y轴负方向一个单位处,观察的目标点center是世界坐标系原点,相机的up方向为世界坐标系z轴正方向。在camera的偏航,俯仰,滚转中,并没有发生平移的变换,于是可以先把eye从(0, -1,0)移动到(0,0,0),那么center相应的就移动到了(0,1,0)。那么camera的forward和世界系y轴重合,side和世界系x轴重合,up和世界系z轴重合。而后进行的heading,pitch,roll可以认为就是相对于世界坐标系,(因为最初的相机的“局部坐标系”和世界坐标系重合),偏航变换(heading)等效于相机绕世界系的z轴旋转,取值区间为[-PI,PI);俯仰变换(pitch)等效于绕世界系的x轴旋转,明显看到这个区间限制为(-PI/2, PI/2),滚转变换(roll)即是camera绕自身的forward做的旋转,这个roll值好像可以取任意值。
     通过一个图说明一下osg中heading,pitch,roll的三个旋转的先后顺序:
无标题.png
黑色坐标系为世界坐标系,绿色坐标系为“相机局部坐标系”。
可以很容易想明白,滚转变换(roll)一定是最后一步变换,因为roll的旋转轴(即新的forward)还没有确定;而到底先进行偏航(heading)变换还是先进行俯仰(pitch)变换,因为osg的坐标系是右手坐标系,思考后也可以看出应先进行俯仰变换(pitch)再进行偏航(heading)变换,如果先进行heading变换则在计算up的时会出现一个奇怪的问题(要将向量(0,0,1)绕z轴旋转一个heading,是否是传说中的万向锁)。

    于是根据先pitch,再heading,最后roll的顺序,有下面代码:
  1.                 double pitch= osg::PI_4;
  2.                 double heading = osg::PI_4;
  3.                 double roll = osg::PI_4;

  4.                 osg::Vec3d foward = osg::Y_AXIS*osg::Matrix::rotate(pitch,osg::X_AXIS);
  5.                 foward = foward*osg::Matrix::rotate(heading, osg::Z_AXIS);
  6.                 foward.normalize();
  7.                 //最后计算roll
  8.                 osg::Vec3d up = osg::Z_AXIS*osg::Matrix::rotate(pitch,osg::X_AXIS);
  9.                 up = up*osg::Matrix::rotate(heading, osg::Z_AXIS);
  10.                 up = up*osg::Matrix::rotate(roll, foward);
  11.                 up.normalize();

  12.                 osg::Vec3d eye;
  13.                 osg::Vec3d center = foward+eye;
  14.                 viewer->getCamera()->setViewMatrixAsLookAt(eye,center,up);
复制代码
顺时针旋转为负,逆时针旋转为正

根据理解在osgthirdpersonview这个例子中测试了一下,当heading, pitch,roll为osg:: PI_4,eye在原点时,有图有真相:
figure.jpg

以上为个人认知,代码仅限讨论,bug带来的经济损失与作者无关。

该用户从未签到

 楼主| 发表于 2013-1-8 20:49:19 | 显示全部楼层
以下为测试代码:
  1. #include <osg/Geometry>
  2. #include <osg/DisplaySettings>
  3. #include <osg/MatrixTransform>
  4. #include <osg/LineWidth>
  5. #include <osgDB/ReadFile>
  6. #include <osgViewer/CompositeViewer>
  7. #include <osgGA/TrackballManipulator>


  8. // Given a Camera, create a wireframe representation of its
  9. // view frustum. Create a default representation if camera==NULL.
  10. osg::Node*
  11. makeFrustumFromCamera( osg::Camera* camera )
  12. {
  13.     // Projection and ModelView matrices
  14.     osg::Matrixd proj;
  15.     osg::Matrixd mv;
  16.     if (camera)
  17.     {
  18.         proj = camera->getProjectionMatrix();
  19.         mv = camera->getViewMatrix();
  20.     }
  21.     else
  22.     {
  23.         // Create some kind of reasonable default Projection matrix.
  24.         proj.makePerspective( 30., 1., 1., 10. );
  25.         // leave mv as identity
  26.     }

  27.     // Get near and far from the Projection matrix.
  28.     const double near = proj(3,2) / (proj(2,2)-1.0);
  29.     const double far = proj(3,2) / (1.0+proj(2,2));

  30.     // Get the sides of the near plane.
  31.     const double nLeft = near * (proj(2,0)-1.0) / proj(0,0);
  32.     const double nRight = near * (1.0+proj(2,0)) / proj(0,0);
  33.     const double nTop = near * (1.0+proj(2,1)) / proj(1,1);
  34.     const double nBottom = near * (proj(2,1)-1.0) / proj(1,1);

  35.     // Get the sides of the far plane.
  36.     const double fLeft = far * (proj(2,0)-1.0) / proj(0,0);
  37.     const double fRight = far * (1.0+proj(2,0)) / proj(0,0);
  38.     const double fTop = far * (1.0+proj(2,1)) / proj(1,1);
  39.     const double fBottom = far * (proj(2,1)-1.0) / proj(1,1);

  40.     // Our vertex array needs only 9 vertices: The origin, and the
  41.     // eight corners of the near and far planes.
  42.     osg::Vec3Array* v = new osg::Vec3Array;
  43.     v->resize( 9 );
  44.     (*v)[0].set( 0., 0., 0. );
  45.     (*v)[1].set( nLeft, nBottom, -near );
  46.     (*v)[2].set( nRight, nBottom, -near );
  47.     (*v)[3].set( nRight, nTop, -near );
  48.     (*v)[4].set( nLeft, nTop, -near );
  49.     (*v)[5].set( fLeft, fBottom, -far );
  50.     (*v)[6].set( fRight, fBottom, -far );
  51.     (*v)[7].set( fRight, fTop, -far );
  52.     (*v)[8].set( fLeft, fTop, -far );

  53.     osg::Geometry* geom = new osg::Geometry;
  54.     geom->setUseDisplayList( false );
  55.     geom->setVertexArray( v );

  56.     osg::Vec4Array* c = new osg::Vec4Array;
  57.     c->push_back( osg::Vec4( 1., 1., 1., 1. ) );
  58.     geom->setColorArray( c );
  59.     geom->setColorBinding( osg::Geometry::BIND_OVERALL );

  60.     GLushort idxLines[8] = {
  61.         0, 5, 0, 6, 0, 7, 0, 8 };
  62.     GLushort idxLoops0[4] = {
  63.         1, 2, 3, 4 };
  64.     GLushort idxLoops1[4] = {
  65.         5, 6, 7, 8 };
  66.     geom->addPrimitiveSet( new osg::DrawElementsUShort( osg::PrimitiveSet::LINES, 8, idxLines ) );
  67.     geom->addPrimitiveSet( new osg::DrawElementsUShort( osg::PrimitiveSet::LINE_LOOP, 4, idxLoops0 ) );
  68.     geom->addPrimitiveSet( new osg::DrawElementsUShort( osg::PrimitiveSet::LINE_LOOP, 4, idxLoops1 ) );

  69.     osg::Geode* geode = new osg::Geode;
  70.     geode->addDrawable( geom );

  71.     geode->getOrCreateStateSet()->setMode( GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED );


  72.     // Create parent MatrixTransform to transform the view volume by
  73.     // the inverse ModelView matrix.
  74.     osg::MatrixTransform* mt = new osg::MatrixTransform;
  75.     mt->setMatrix( osg::Matrixd::inverse( mv ) );
  76.     mt->addChild( geode );

  77.     return mt;
  78. }


  79. int
  80. main( int argc,
  81.       char ** argv )
  82. {
  83.     osg::ArgumentParser arguments( &argc, argv );

  84.     osg::ref_ptr< osg::Group > root = new osg::Group;

  85.     // Child 0: We'll replace this every frame with an updated representation
  86.     //   of the view frustum.
  87.     root->addChild( makeFrustumFromCamera( NULL ) );

  88.     osg::ref_ptr< osg::Node > scene;
  89.     scene = osgDB::readNodeFiles( arguments );
  90.     if (!scene)
  91.     {
  92.         // User didn't specify anything, or file(s) didn't exist.
  93.         // Try to load the cow...
  94.         osg::notify( osg::WARN ) << arguments.getApplicationName() << ": Could not find specified files. Trying "cow.osgt" instead." << std::endl;
  95.         if ( !(scene = osgDB::readNodeFile( std::string( "axes.osg" ) ) ) )
  96.         {
  97.             osg::notify( osg::FATAL ) << arguments.getApplicationName() << ": No data loaded." << std::endl;
  98.             return 1;
  99.         }
  100.     }
  101.     root->addChild( scene.get() );


  102.     osgViewer::CompositeViewer viewer( arguments );
  103.     // Turn on FSAA, makes the lines look better.
  104.     osg::DisplaySettings::instance()->setNumMultiSamples( 4 );

  105.     // Create View 0 -- Just the loaded model.
  106.     {
  107.         osgViewer::View* view = new osgViewer::View;
  108.         viewer.addView( view );

  109.         view->setUpViewInWindow( 10, 10, 640, 480 );
  110.         view->setSceneData( scene.get() );
  111.         //view->setCameraManipulator( new osgGA::TrackballManipulator );
  112.     }
  113.    
  114.     // Create view 1 -- Contains the loaded moel, as well as a wireframe frustum derived from View 0's Camera.
  115.     {
  116.         osgViewer::View* view = new osgViewer::View;
  117.         viewer.addView( view );

  118.         view->setUpViewInWindow( 10, 510, 640, 480 );
  119.         view->setSceneData( root.get() );
  120.         view->setCameraManipulator( new osgGA::TrackballManipulator );
  121.     }

  122.     while (!viewer.done())
  123.     {
  124.         // Update the wireframe frustum
  125.         root->removeChild( 0, 1 );
  126.         root->insertChild( 0, makeFrustumFromCamera(
  127.                 viewer.getView( 0 )->getCamera() ) );

  128.                 double pitch= osg::PI_4;
  129.                 double heading = osg::PI_4;
  130.                 static double roll = osg::PI_4;
  131.                 /**roll递增,用于测试roll的变化,
  132.                   *pitch,heading同理可测
  133.                   *注意测试pitch时区间限制: (-osg::PI_2, osg::PI_2)开区间
  134.                   *heading的限制: [-osg::PI, osg::PI)左闭右开
  135.                   */
  136.                 roll+=0.01;

  137.                 osg::Vec3d foward = osg::Y_AXIS*osg::Matrix::rotate(pitch,osg::X_AXIS);
  138.                 foward = foward*osg::Matrix::rotate(heading, osg::Z_AXIS);
  139.                 foward.normalize();
  140.                 //最后roll变换
  141.                 osg::Vec3d up = osg::Z_AXIS*osg::Matrix::rotate(pitch,osg::X_AXIS);
  142.                 up = up*osg::Matrix::rotate(heading, osg::Z_AXIS);
  143.                 up = up*osg::Matrix::rotate(roll, foward);
  144.                 up.normalize();

  145.                 osg::Vec3d eye;
  146.                 osg::Vec3d center = foward+eye;
  147.                 viewer.getView(0)->getCamera()->setViewMatrixAsLookAt(eye,center,up);

  148.         viewer.frame();
  149.     }
  150.     return 0;
  151. }
复制代码

该用户从未签到

发表于 2013-1-8 22:37:50 | 显示全部楼层
最近也在思考这个问题,关注ing

该用户从未签到

发表于 2013-1-9 11:22:55 | 显示全部楼层
欢迎分享自己的学习经验

该用户从未签到

发表于 2013-1-9 14:19:16 | 显示全部楼层
好东西 支持下!其实我也有点糊涂

该用户从未签到

发表于 2013-1-11 11:33:11 | 显示全部楼层
本帖最后由 s99 于 2013-1-11 11:34 编辑

欧拉角有多种变换顺序,不同领域的使用也不尽相同。
在飞行仿真领域,按照国家标准(GB/T14410)规定如下:
俯仰角(pitch)体轴系x轴(相当于osg的local的y轴)与水平面(相当于osg的world的xy平面)之间的夹角,范围-90~90度
滚转角(roll)体轴系xz平面(相当于osg的local的yz平面)与铅垂平面(相当于osg的world的yz平面)的夹角,范围-180~180度
偏航角(heading)体轴系x轴(相当于osg的local的y轴)在水平面内的投影与地轴系x轴(osg的world的y轴)之间的夹角,范围0~360度

抛开轴系方向定义的不同,这种欧拉角的定义其实与大部分仿真系统定义的一致,也就是一般所说的heading、pitch、roll(hpr)
在这种欧拉角的定义下,旋转顺序(从地轴系到体轴系,对于osg为从world到local)一定是heading、pitch、roll而不是楼主所说的pitch、heading、roll

万向锁对应于当pitch为90度或-90度时,roll和heading无意义,此时osg的y轴在水平面的投影是一个点,无法得到其与一个轴的夹角,而飞机无论怎么滚转其对称面与地平面之间的夹角都是90度。这在飞行仿真中叫做“奇异性”,不同欧拉变换顺序的奇异点不同(可能并不是出现在pitch=90),利用不同变换顺序的欧拉角奇异点不同的特点,将两种欧拉法组合起来,也可以解决仿真中的奇异性(万向锁)问题,这种方法叫做双欧法,只是这种方法很少用,仿真中用四元数的居多。

该用户从未签到

 楼主| 发表于 2013-1-11 13:14:00 | 显示全部楼层
本帖最后由 woshijiameizhou 于 2013-1-11 13:15 编辑

感谢s99的回复。看您的回复后感觉标准中有两点疑问:偏航角(heading)的范围0~360这个设计不是很合理,比如一个人要观察他的正右方,他只需向右转90°,而不是向左转270°(所以原帖子中认为是heading的范围是[-180,180)或者(-180,180]);疑问2就是标准中滚转角(roll)的定义是否有误,用下面这个图说明:
无标题.jpg
世界坐标系为OXYZ,“局部坐标系”为Ox'y'z',其中OZ与Oz‘重合,Oy'绕Z轴顺时针旋转alpha,这个alpha角既符合您在回复中提到的heading的定义,同时也符合roll的定义(即也是y’z’面和YZ面夹的二面角)。我对hpr的理解完全是游击队作战,发帖的目的也在于抛砖引玉。感谢你的回复。

该用户从未签到

发表于 2013-1-12 08:33:34 | 显示全部楼层
heading的范围只是习惯问题了,0~360只是航空领域的定义而已,0正北、90正东、180正南、270正西,范围-180~180同样没问题

这个roll是我写错了,在osg中不是yz平面,是osg的local的yz平面与过local的y轴的铅垂平面(不是osg的world的yz平面)的夹角
您需要登录后才可以回帖 登录 | 注册

本版积分规则

OSG中国官方论坛-有您OSG在中国才更好

网站简介:osgChina是国内首个三维相关技术开源社区,旨在为国内更多的技术开发人员提供最前沿的技术资讯,为更多的三维从业者提供一个学习、交流的技术平台。

联系我们

  • 工作时间:09:00--18:00
  • 反馈邮箱:1315785073@qq.com
快速回复 返回顶部 返回列表