Skip to content

Latest commit

 

History

History
90 lines (64 loc) · 10.4 KB

动态更改.md

File metadata and controls

90 lines (64 loc) · 10.4 KB

拣选(cull)遍历中关联了渲染图形中的几何数据和渲染状态信息,它们将在绘制(draw)遍历中进行处理。osgViewer库支持多线程模式,每一个线程均独立地运行拣选及绘制遍历。出于性能优化的考虑,OSG并没有为了线程的安全性增设内存锁,而是要求用户程序只可在拣选及绘制遍历的时域之外修改场景图形。

有几种方法可以确保用户的修改不会与拣选及绘制线程发生冲突。一个简单的方案是,在Viewer::frame()的调用之外进行场景图形的修改,这需要在主渲染循环中添加额外的代码。但如果用户希望自己的程序更加整洁和规范的话,可以选择在更新遍历中进行场景的修改操作。

下面介绍一些与场景图形动态更改相关的基本技术。

  • 出于性能优化和线程安全性的考虑,用户需要通知OSG,场景图形的哪些部分是可能要进行修改的。用户可以通过设置 Object对象(Node,Drawable,StateSet等)的数据变度(data variance)属性来完成这一工作。
  • OSG允许用户程序为Node和Drawable设置回调(callback)。OSG将在特定遍历中执行这些回调。例如要在更新遍历中修改Node或者Drawable对象的值,就可以通过设置更新回调(update callback)来完成。
  • 用户程序有时不能预知场景图形的哪一部分需要修改。这时需要搜索整个场景图形来查找特定的节点,或者由用户使用鼠标等输入设备来选择一个节点。

数据变度

osgViewer支持的多线程模型允许用户程序主循环不必等到绘制遍历结束就可以继续运行。这就是说 Viewer::frame()方法在绘制遍历仍未结束的时候就可以返回。换句话说,上一帧的绘制遍历可以与下一帧的更新遍历产生交叠。仔细考虑这一线程模型的实现的话,会发现它几乎很难避免与绘制遍历线程的冲突。不过,OSG提供了osg:: Object:: DataVariance()方法作为这一问题的解决方案。

程序崩溃的原因: 当用户开发了动态更改场景图形的代码时,可能会在修改场景图形时遇到程序崩溃或者段错误的情况。诸如此类的问题往往都是由于用户在拣选和绘制遍历过程中修改了场景图形的数据,而造成系统崩溃。

要设置一个Object对象的数据变量,可以调用setDataVariance()并设置输入参数为Object:: DataVariance枚举量。初始状态下,变度的值是UNSPECIFIED。 用户程序可以将数据变度更改为STATIC 或者DYNAMIC。OSG将确保绘制遍历在所有的DYNAMIC节点和数据处理完成后才会返回。同样,由于绘制遍历在函数返回后仍然可以继续渲染场景图形,OSG将确保此时只有STATIC数据可以继续进行图形渲染。如果用户的场景图形包含了很少的DYNAMIC数据,那么绘制遍历可以很快返回,保证用户程序可以继续执行其它的任务。

回调

OSG允许用户设置Node和Drawable对象的回调类。Node可以在OSG执行更新和拣选遍历时进行回调,而Drawable可以在拣选和绘制遍历时进行回调。 本节将介绍在更新遍历中使用osg::NodeCallback动态修改 Node 节点的方法。 OSG的回调接口则基于一定的回调设计模式[Gamma95]。 如果要使用 NodeCallback,用户程序需要执行下面的步骤:

  • 从NodeCallback继承一个新的类。
  • 重载NodeCallback:: operator()方法。使用这个方法来实现场景图形的动态更改。
  • 将用户从NodeCallback继承的类实例化,然后使用Node:: setUpdateCallback()方法关联到将要修改的Node。在每个更新遍历过程中,OSG都会调用派生类中的operator()方法,从而允许用户程序对Node进行修改。

OSG向operator()方法传递了两个参数。第一个是回调类所关联的Node的地址,也就是用户回调将在 operator()方法中进行动态更改的Node节点。第二个 参数是osg::NodeVisitor对象的地址。

NodeVisitor类

NodeVisitor类是OSG对于访问器(visitor)设计思想[Gamma95]的具体实现。从本质上说,NodeVisitor类遍历了一个场景图形并为每一个被访问节点调用特定的函数。这一简单的技术却是许多OSG操作的基类,包括osgUtil:: Optimizer,osgUtil库的几何体处理类,以及文件输出类。OSG使用osgUtil::UpdateVisitor类(继承自NodeVisitor)来实现更新遍历。而在前面章节的代码中,UpdateVisitor恰恰正是调用了NodeCallback:: operator()方法的NodeVisitor类。总之,NodeVisitor在OSG的应用中无处不在。

NodeVisitor是一个基类,用户程序无法直接将其实例化。但是用户程序可以使用OSG提供的任何 NodeVisitor派生类,也可以自己编写继承自NodeVisitor的类的代码。NodeVisitor类包含了一些经过重载的apply()方法,其输入参数涵盖了大部分OSG的节点类型。当一个NodeVisitor对象遍历整个场景图形时,它将会为每个被访问的节点调用其相应的apply()方法。

允许 NodeVisitor 遍历:

缺省情况下,NodeVisitor基类禁止执行遍历。因此在你的派生类中,需要使用枚举量NodeVisotor::TRAVERSE_ALL_CHILDREN来初始化基类,以允许执行遍历。否则,OSG将不会调用用户的apply()方法。

用户选择

大多数的3D程序都需要某种形式的用户选择功能,终端用户可以以此来交互地选择当前被显示的画面的某个一部分。用户选择中最简单的形式为,用户将鼠标移动到场景中特定的位置,并点击鼠标。程序内部将进行运算,以便将2D的鼠标XY坐标位置映射到正确的3D场景图形节点上,并保存节点地址以便将来使用。 从本质上说,OSG 程序通过两个步骤来实现用户选择:

  • 接收鼠标事件。osgGA 库提供了允许程序接收鼠标事件的事件类,它具备平台无关的特性。
  • 判断场景图形的哪个部分被鼠标光标覆盖。osgUtil库提供了一种相交集(intersection)类,可以在鼠标XY坐标的周围创建包围盒,并判断包围盒与场景图形的相交情况。osgUtil将按照由前至后的顺序返回与包围盒相交的节点列表。

要实现接收鼠标事件并实现用户选择的功能,需要经过以下几个步骤:

  • 从GUIEventHandler继承新的类。重载handle()方法。
  • 在 handle()中,检查GUIEventAdapter参数传递的事件类型,并针对需要的事件类型执行相应的操作。方法返回true时将阻止其它事件处理器继续接收事件消息。
  • 在渲染之前,创建事件处理器类的实例,并使用addEventHandler()方法添加到视口中。OSG将会把视口作为GUIActionAdapter参数传递给handle()方法。

上述的技术并不局限于使用鼠标进行选择。用户程序可以尝试实现与TrackballManipulator类相似的接收鼠标事件的类。另外,也可以接收键盘事件并实现对按键的响应操作。

交集

你可以将通过点击鼠标的节点选择想象成是从鼠标(光标)位置向场景中发射了一条射线。被鼠标选中的场景部分将与射线有一个交集。如果场景是由线和点元素组成的,那么射线的交运算可能无法符合用户的实际选择,因为鼠标的位置几乎无法与这些图元产生精确的空间交集。此外,在典型的透视渲染中,射线 交运算的精度将与观察者所处的距离成反比。

OSG使用一种名为多胞体(polytope)的金字塔形包围盒代替射线,以克服上述的问题。这种金字塔形的顶峰位于视点,其中心轴直接穿过鼠标(光标)的位置。它距离视点的宽度是由视场和程序控制的宽度参数决定的。

OSG使用场景图形的自顶向下的继承结构,从而避免了OpenGL普遍存在的“迟钝的”选择特性,而在本地CPU上进行高效的计算。osgUtil::IntersectionVisitor类继承自NodeVisitor,它可以检测每个定点的包围盒与交集包围盒的关系,并允许在某个子图形不可能存在有交集的子节点时,跳过该子图形的遍历。

用户可以设置IntersectionVisitor类并使用几种不同的几何结构进行交集检测,例如平面和线段。其构造函数使用osgUtil::Intersector作为输入参数,Intersector类定义了选择操作的几何体并执行实际的交集测试。Intersector是一个纯虚基类,用户程序无法将其实例化。而osgUtil库从Intersector派生了一些代 表不同几何结构的新类,例如osgUtil:: PolytopeIntersector,也就是前文所述的、较理想的鼠标点选判定模型。

有些程序需要拾取单独的顶点和多边形;而有些程序只需要简单地获取那些包含了被选节点的Group或Transform父节点。IntersectionVisitor返回osg::NodePath对象来满足这些需求。NodePath是一个std::vectorosg::Node向量,它表示沿着从根节点到叶节点的、呈现出一定排列层次的节点路线。如果用 户程序需要获取中间的Group组节点,那么只需要从后向前搜索满足程序要求的节点即可。

综上所述,要实现OSG中的鼠标选择操作,需要按照如下的步骤编写代码:

  • 创建并设置PolytopeIntersector,其中的鼠标位置应当使用GUIEventAdapter中经过归一化的数据。
  • 创建IntersectionVisitor对象,并将PolytopeIntersector作为其构造函数的输入参数。
  • 由场景图形的根节点加载IntersectionVisitor,一般来说是通过Viewer中的Camera对象,如下面的代码所示:
osgUtil::PolytopeIntersector* picker = new osgUtil::PolytopeIntersector(osgUtil::Intersector::PROJECTION, x-w, y-h, x+w, y+h); osgUtil::IntersectionVisitor iv(picker);
viewer->getCamera()->accept( iv );
if (picker->containsIntersections())
{
    osg::NodePath& nodePath = picker->getFirstIntersection().nodePath;
    unsigned int idx = nodePath.size();
    while (idx--)
    {
        // 查找交集节点路径中的最后一个 MatrixTransform;
        // 它就是将要与回调相关联的选择结果。
        osg::MatrixTransform* mt = dynamic_cast<osg::MatrixTransform*>(nodePath[ idx ] ); 
        if (mt == NULL)
            continue;
        // 到了这里,
        // 说明已在节点路径中找到了所需的 MatrixTransform。
        ....
    }
}

  • 如果PolytopeIntersector的返回值包含了交集,那么可以获取返回的NodePath并搜索符合要求的节点。