OpenSceneGraph:场景图和智能指针

图片

引言


上一篇文章中,我们从源头上查看了OpenSceneGraph程序集,并编写了一个基本示例,其中灰色平面悬挂在一个空的紫色世界中。 我同意,不是太令人印象深刻。 但是,正如我之前说的,在这个小例子中,该图形引擎基于一些主要概念。 让我们更详细地考虑它们。 下面的材料使用了亚历山大·鲍勃科夫Alexander Bobkov)的博客中有关OSG的插图(可惜作者放弃了有关OSG的文章...)。 本文还基于OpenSceneGraph 3.0书中的材料和示例 初学者指南

我必须说,先前的出版物受到了一些批评,我对此表示部分赞同-所发表的材料没有说明,是脱离上下文的。 我会尽力弥补这一遗漏。

1.简要介绍场景图及其节点


引擎的中心概念是所谓的场景图 (它被卡在框架本身的名称中并非偶然)-一种分层的树状结构,可让您组织三维场景的逻辑和空间表示。 场景图包含根节点及其关联的中间节点和终端节点

举个例子



此图描绘了一个场景,其中包括房屋和桌子。 房子具有一定的几何表示形式,并且相对于与根节点(root)关联的某个基本坐标系以某种方式位于空间中。 该表还通过某种几何形状来描述,这些几何形状相对于房屋以某种方式定位,并且与房屋(相对于根节点)一起定位。 所有具有共同属性的节点(因为它们从一个osg :: Node类继承),因此根据其功能用途被分为类型

  1. 组节点(osg :: Group)-是所有中间节点的基类,旨在将其他节点组合为组
  2. 转换节点(osg :: Transform及其后代)-用于描述对象坐标的转换
  3. 几何节点(osg :: Geode)-场景图的终端(叶)节点,其中包含有关一个或多个几何对象的信息。

OSG中场景对象的几何形状在对象自己的局部坐标系中描述。 位于此对象和根节点之间的变换节点执行矩阵坐标变换,以获取对象在基本坐标系中的位置。

节点执行许多重要功能,尤其是存储对象显示的状态,并且此状态仅影响与此节点关联的子图。 多个回调可以与场景图中的节点关联,事件处理程序使您可以更改节点和与之关联的子图的状态。

与引擎在屏幕上获得最终结果相关联的场景图上的所有全局操作都是由引擎通过定期深度遍历图自动执行的。

上次检查的示例中,我们的场景由单个对象组成-从文件加载的飞机模型。 展望未来,我将说这个模型是场景图的叶子节点。 它被紧密地焊接到发动机的整体基础坐标系上。

2. OSG内存管理


由于场景图的节点存储了大量有关场景对象及其上的操作的数据,因此有必要动态分配内存以存储此数据。 在这种情况下,操作场景图(例如,删除场景的某些节点)时,需要仔细监视该图的已删除节点是否已不再处理。 这个过程总是伴随着错误和费时的调试,因为开发人员很难跟踪指向对象的指针引用了现有数据,应该删除哪些指针。 如果没有有效的内存管理,则很可能发生分段错误和内存泄漏。

内存管理是OSG中的一项关键任务,其概念基于两点:

  1. 内存分配:确保分配存储对象所需的内存量。
  2. 释放内存:不需要时将分配的内存返回给系统。

许多现代编程语言,例如C#,Java,Visual Basic .Net等,都使用所谓的垃圾回收器释放分配的内存。 C ++语言的概念没有提供这种方法,但是,我们可以通过使用所谓的智能指针来模仿它。

如今,C ++在其工具库中拥有智能指针,这被称为“开箱即用”(并且C ++ 17标准已经设法摆脱了一些过时类型的智能指针的语言),但并非总是如此。 最早的官方OSG版本0.9诞生于2002年,距离第一个正式发布还差三年。 当时,C ++标准尚未提供智能指针,即使您相信一个历史题外话 ,该语言本身也正处于艰难时期。 因此,在OSG中实现的以其自身的智能指针形式出现的自行车完全不足为奇。 该机制已深入集成到发动机的结构中,因此从一开始就必须了解其运行。

3. osg :: ref_ptr <>和osg ::引用的类


OSG提供了自己的智能指针机制,该机制基于osg :: ref_ptr <>模板类来实现自动垃圾收集。 为了正常运行,OSG提供了另一个osg :: Referenced类,用于管理对它们的引用计数的内存块。

osg :: ref_ptr <>类提供了几种运算符和方法。

  • get()是返回原始指针的公共方法,例如,当使用osg :: Node模板作为参数时,此方法将返回osg :: Node *。
  • 运算符*()实际上是取消引用运算符。
  • 运算符->()和运算符=()-在访问由该指针描述的对象的方法和属性时,可以使用osg :: ref_ptr <>作为经典指针。
  • operator ==(),operator!=()和operator!()-允许您对智能指针执行比较操作。
  • 有效的()是一个公共方法,如果托管指针具有正确的值(非NULL),则返回true。 如果some_ptr是智能指针,则表达式some_ptr.valid()等效于表达式some_ptr!= NULL。
  • release()是一个公共方法,当您要从函数返回托管地址时很有用。 稍后将对其进行详细描述。

osg :: Referenced类是场景图所有元素(例如节点,几何体,渲染状态和放置在舞台上的其他对象)的基类。 因此,创建场景的根节点后,我们间接继承了osg :: Referenced类提供的所有功能。 因此,在我们的程序中有一个公告

osg::ref_ptr<osg::Node> root; 

osg :: Referenced类包含一个整数计数器,用于引用分配的内存块。 该计数器在类构造函数中初始化为零。 创建osg :: ref_ptr <>对象时,它增加一。 一旦删除对该指针描述的对象的任何引用,此计数器就会减少。 当任何智能指针停止引用该对象时,该对象将自动销毁。

osg ::引用的类具有三个公共方法:

  • ref()是一种公共方法,其引用计数增加1。
  • unref()是一种公共方法,减少了1个引用计数。
  • referenceCount()是一个公共方法,它返回引用计数器的当前值,这在调试代码时很有用。

从osg ::引用的所有类中都可以使用这些方法。 但是,应该记住,手动控制链接计数器会导致不可预测的后果,使用此链接,您应该清楚地了解自己在做什么。

4. OSG如何收集垃圾以及为什么需要垃圾


使用智能指针和垃圾回收有几个原因:

  • 最小化严重错误:使用智能指针可使您自动分配和释放内存。 没有危险的原始指针。
  • 有效的内存管理:在不需要对象时立即释放为该对象分配的内存,这可以经济地使用系统资源。
  • 简化应用程序调试:能够清楚地跟踪到对象的链接数量,我们有机会进行各种优化和实验。

假设场景图由一个根节点和几个级别的子节点组成。 如果使用osg :: ref_ptr <>类管理根节点和所有子节点,则应用程序只能跟踪指向根节点的指针。 删除此节点将导致顺序自动删除所有子节点。



智能指针可用作局部变量,全局变量,类成员,并在智能指针超出范围时自动减少引用计数。

OSG开发人员强烈建议在项目中使用智能指针,但是您应注意一些基本要点:

  • 可以仅在堆上创建osg :: Referenced及其派生实例。 它们不能作为局部变量在堆栈上创建,因为这些类的析构函数被声明为已保护。 举个例子

 osg::ref_ptr<osg::Node> node = new osg::Node; //  osg::Node node; //  

  • 您可以使用常规C ++指针创建临时场景节点,但是,这种方法是不安全的。 最好使用智能指针来确保场景图得到正确管理。

 osg::Node *tmpNode = new osg::Node; //  ,  ... osg::ref_ptr<osg::Node> node = tmpNode; //         ! 

  • 当节点直接或间接通过多个级别引用自身时,无论如何都不要在树中使用循环链接场景



在场景图的示例图中,Child 1.1节点引用自身,Child 2.2节点也引用Child 1.2节点。 此类链接可能导致链接数量的错误计算和程序的不确定行为。

5.跟踪被管理对象


为了说明OSG中智能指针机制的操作,我们编写以下综合示例

主文件

 #ifndef MAIN_H #define MAIN_H #include <osg/ref_ptr> #include <osg/Referenced> #include <iostream> #endif // MAIN_H 

main.cpp

 #include "main.h" class MonitoringTarget : public osg::Referenced { public: MonitoringTarget(int id) : _id(id) { std::cout << "Constructing target " << _id << std::endl; } protected: virtual ~MonitoringTarget() { std::cout << "Dsetroying target " << _id << std::endl; } int _id; }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; osg::ref_ptr<MonitoringTarget> anotherTarget = target; std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; return 0; } 

我们创建一个osg ::引用后代类,该类除了在构造函数和析构函数中不执行任何操作外,它报告已创建其实例并显示在创建该实例时确定的标识符。 使用智能指针机制创建类的实例

 osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); 

接下来,我们显示目标对象的参考计数器

 std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; 

之后,创建一个新的智能指针,为其分配前一个指针的值

 osg::ref_ptr<MonitoringTarget> anotherTarget = target; 

并再次显示参考计数器

 std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; 

让我们看看通过分析程序输出得到的结果

 15:42:39:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Dsetroying target 0 15:42:42:   

当类构造函数启动时,会显示一条相应的消息,告诉我们该对象的内存已分配,构造函数运行良好。 此外,在创建智能指针之后,我们看到所创建对象的引用计数器增加了一个。 创建一个新的指针,为其分配旧指针的值,实际上是在创建指向同一对象的新链接,因此,参考计数器会增加另一个。 程序退出时,将调用MonitoringTarget类的析构函数。



让我们通过将这样的代码添加到main()函数的末尾来进行另一个实验

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = new MonitoringTarget(i); } 

导致这样的“排气”程序

 16:04:30:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Constructing target 1 Dsetroying target 1 Constructing target 2 Dsetroying target 2 Constructing target 3 Dsetroying target 3 Constructing target 4 Dsetroying target 4 Dsetroying target 0 16:04:32:   

我们使用智能指针在循环主体中创建几个对象。 由于在这种情况下指针的作用域仅扩展到循环的主体,因此在退出时,将自动调用析构函数。 很显然,这不会发生,我们将使用通常的指针。

自动释放内存是使用智能指针的另一个重要功能。 由于osg ::引用的派生类析构函数受到保护,因此我们无法显式调用delete运算符来删除对象。 删除对象的唯一方法是重置指向该对象的链接数。 但是,在多线程数据处理过程中,我们的代码变得不安全-我们可以从另一个线程访问已删除的对象。

幸运的是,OSG借助其对象删除调度程序为该问题提供了解决方案。 该调度程序基于osg :: DeleteHandler类的使用。 它的工作方式是它不会立即执行删除对象的操作,而是会在一段时间后执行该操作。 临时存储所有要删除的对象,直到安全删除为止,然后立即将它们全部删除。 osg :: DeleteHandler删除调度程序由OSG渲染后端控制。

6.从功能返回


将以下函数添加到示例代码中

 MonitoringTarget *createMonitoringTarget(int id) { osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(id); return target.release(); } 

并使用对此函数的调用替换对循环中对new运算符的调用

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = createMonitoringTarget(i); } 

release()调用会将对对象的引用数减少为零,但不是删除内存,而是直接将实际指针返回到分配的内存。 如果将此指针分配给另一个智能指针,则不会发生内存泄漏。

结论


场景图和智能指针的概念是理解操作原理的基础,因此也是有效使用OpenSceneGraph的基础。 关于OSG智能指针,请记住,当

  • 预计该设施将长期存放。
  • 一个对象存储到另一个对象的链接
  • 您必须从函数返回指针

本文提供了示例代码。

待续...

Source: https://habr.com/ru/post/zh-CN429914/


All Articles