开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天
1 概述
坐标系变换是计算机图形处理的一个基础研究方向。三维实体对象需要经过一系列的坐标变换才能正确、真实地显示在屏幕上。在一个场景中,当读者对场景中的物体进行各种变换及相关操作时,坐标系变换是非常频繁的。
坐标系变换通常包括世界坐标系—物体坐标系变换、物体坐标系—世界坐标系变换和世界坐标系—屏幕坐标系变换。屏幕坐标系是一个二维平面坐标系,即显示器平面,是非常标准的笛卡儿坐标系的第一象限区域。
使用OSG开发时,经常会用到坐标系变换,下面介绍在OSG中坐标系变换的具体实现,可能不能面面俱到,但具有普遍应用性。
2 世界坐标系—物体坐标系变换
世界坐标系—物体坐标系变换相对比较容易,它描述的问题主要是关于物体本身的。假设在世界坐标系中,一个人正准备走向一栋建筑,那么他就面临世界坐标系到物体坐标系的变换过程,变换过程中面临的问题是人相对建筑物的朝向、人相对建筑物的距离及人的移动方向等一系列的问题。
上面的假设读者或许会觉得非常熟悉,它基本等同于基本变换的过程。在OSG中,已有如下相关的类实现了基本变换过程:
osg:PositionAttitude Transform // 位置变换类
osg:MatrixTransform //矩阵变换类
这两个类实现的效果基本是相同的,只是数据的表达方式有区别,这样有利于处理各种数据的变换。后面的章节还会详细介绍,这里暂且留一个如何使用这两个类的疑问。或许学习这一章读者会有很多实现方面的疑问,不过没关系,通读全书后将不再疑惑。
通过上面的两个类可以很方便地实现世界坐标系—物体坐标系的变换过程应该是容易理解的。世界坐标系—物体坐标系变换的意义在于简化了世界坐标系下变换的运算,当面对非常大的场景时,这种变换在一定程度上可以减少数据的运算量和提高场景的渲染效率。
3 物体坐标系—世界坐标系变换
物体坐标系—世界坐标系变换不能简单地理解为世界坐标系—物体坐标系变换的逆过程。物体坐标系。世界坐标系变换描述的问题是处于世界坐标系中的物体。假设在物体坐标系中,一栋建筑物中有一个人,如何确定人在世界坐标系中的位置信息就是物体坐标系—世界坐标系变换所面对的问题。
物体坐标系—世界坐标系变换是有一定难度的。对于场景图形中某一个OSG 节点,它和根节点之间可能存在一些变换节点,那么如何获取该节点在世界坐标系中的位置就显得非常困难。
但是,在场景图形中,每一个节点都有自己的父节点且有自己的变换矩阵,这些变换矩阵包含了相对坐标数据。那么,计算某一特定节点在世界坐标系下的坐标,只需要将该节点的根节点和该节点之间的所有变换矩阵相乘即可。
在OSG 中有多种方式来实现物体坐标系—世界坐标系的变换,如回调、访问器等,用访问器实现是一种方便可操控的方式,相对而言,回调在一定程度上不具备可操控行,且会因为增加额外开销而影响渲染效率,但每帧都会自动计算矩阵变换。
访问器通过遍历的方式记录场景中节点的路径,并计算路径上矩阵变换的世界坐标,最终返回一个矩形式表示的世界坐标。下面具体实现该访问器,读者不必理解每一行代码,但需要理解实现的基本思路和原理,在以后的学习中,读者将逐渐熟悉代码,这些代码会经常用到。代码实现如下所示。
00001 // 该访问器类用于返回某个节点的世界坐标
00002 // 从起始节点开始向根节点遍历,并将遍历的节点记录到nodePath中
00003 // 第一次到达根节点之后,记录起始点到根节点的节点路径
00004 // 获取所有世界坐标矩阵之后,即获得节点的世界坐标
00005 class GetWorldCoordinateOfNodeVisitor:public osg::NodeVisitor
00006 {
00007 public:
00008 Get WorldCoordinateOfNode Visitor():
00009 osg:NodeVisitor(NodeVisitor::TRAVERSE_PARENTS),done(false)
00010 {
00011 wcMatrix=new osg::Matrixd();
00012 }
00013 virtual void apply(osg::Node &node)
00014 {
00015 if(ldone)
00016
00017 // 到达根节点,此时节点路径也已记录完整
00018 if(0-node.getNumParents())
00019 {
00020 wcMatrix->set(osg::computeLocalToWorld(this->getNodePath());
00021 done=true;
00022 }
00023 //继续遍历
00024 traverse(node);
00025 }
00026
00027 //返回世界坐标矩阵
00028 osg:Matrixd*giveUpDaMat()
00029 {
00030 return wcMatrix;
00031 }
00032 private:
00033 bool done;
00034
00035 osg:Matrix*wcMatrix;
00036};
00037
00038 //计算场景中某个节点的世界坐标,返回osg:Matrix格式的世界坐标
00039 //创建用于更新世界坐标矩阵的访问器之后,即获取该矩阵
00040 osg::Matrixd* getWorldCoords(osg::Node*node)
00041 {
00042 GetWorldCoordinateOfNodeVisitor*ncv=new Get WorldCoordinateOfNode Visitor();
00043
00044 if(node&&ncv)
00045 {
00046 //启用访问器
00047 node~>accept(*ncv);
00048
00049 return ncv~>giveUpDaMat();
00050 }
00051 else
00052 {
00053 return NULL;
00054 }
00055}
程序清单2 00001//该访问器类用于返回某个节点的世界坐标
00002//从起始节点开始向根节点遗历,并将遍历的节点记录到nodePath中
00003//第一次到达根节点之后,记录起始点到根节点的节点路径
00004//获取所有世界坐标矩阵之后,即获得节点的世界坐标
00005 class GetWorldCoordinateOfNodeVisitor:public ogNodeVisitor
下面对程序中的代码进行简单的说明。
☑ 第5~36行:实现父节点遍历访问器,记录节点路径 nodePath,计算各个节点的变换矩阵, 返回节点的世界坐标。
☑ 第9行:注意设置节点的遍历模式为 NodeVisitor::TRAVERSE_PARENTS,向当前节点及父节点遍历。
☑ 第33行:变量done判断是否已经遍历到根节点。
☑ 第35行:变量wcMatrix 保存节点的世界坐标矩阵。
☑ 第42行:创建一个节点访问器GetWorldCoordinateOfNode Visitor.
☑ 第47行:注意启用节点访问器,开始遍历。
☑ 第49行:遍历完成后,返回世界坐标矩阵。
4 世界坐标系—屏幕坐标系变换
在场景中,所有的实体对象需要经过一系列的坐标变换才能正确显示在屏幕上,这些变换主要包括模型变换(将实体对象正确地放置在场景中)、投影变换(将场景中的实体对象投影到垂直于视线方向的二维成像平面上)和视口变换(投影变换之后得到的顶点需要经过视区变换才能得到最后的窗口坐标)。屏幕坐标是二维坐标,世界坐标是三维坐标,因此,正确的世界坐标系—屏幕坐标系变换就非常必要。
通过前面的介绍可知,世界坐标系—屏幕坐标系变换主要有3个步骤,即模型变换、投影变换和视口变换,由模型变换和投影变换得到归一化的设备坐标,最后由视口变换得到屏幕窗口坐标。