OpenSceneGraph: Scene Graph and Smart Pointers

  • Tutorial
image

Introduction


In the last article, we looked at the build of OpenSceneGraph from source and wrote an elementary example in which a gray plane hangs in an empty purple world. I agree, not too impressive. However, as I said before, in this small example there are main concepts on which this graphic engine is based. Consider them in more detail. In the material below, illustrations from Alexander Bobkov's blog about OSG are used (it is a pity that the author abandoned writing about OSG ...). The article is also based on the material and examples from the book OpenSceneGraph 3.0. Beginner's Guide

I must say that the previous publication was subjected to some criticism, with which I partially agree - the material was left unsaid and taken out of context. I will try to correct this omission under the cut.

1. Briefly about the scene graph and its nodes


The central concept of the engine is the so-called scene graph (it is not by chance that it is framed into the very name of the framework) - a tree-like hierarchical structure that allows you to organize the logical and spatial representation of the three-dimensional scene. The scene graph contains the root node and its associated intermediate and end nodes or nodes .

for example



This graph depicts a scene consisting of a house and a table located in it. The house has a certain geometric representation and is in a certain way located in space relative to a certain basic coordinate system associated with the root node (root). The table is also described by some geometry, located in some way relative to the house, and together with the house - relative to the root node. All nodes possessing common properties, because they are inherited from the same class osg :: Node, are divided into types according to their functional purpose

  1. Group nodes (osg :: Group) are the base class for all intermediate nodes and are intended for combining other nodes into groups.
  2. Transformation nodes (osg :: Transform and its heirs) are intended to describe the transformation of object coordinates
  3. Geometrical nodes (osg :: Geode) are terminal (leaf) nodes of a scene graph containing information about one or several geometric objects.

The geometry of scene objects in OSG is described in its own local coordinate system. Transformation nodes located between this object and the root node implement matrix coordinate transformations to obtain the position of the object in the base coordinate system.

Nodes perform many important functions, in particular, they store the display state of objects, and this state affects only the subgraph associated with this node. Several callbacks and event handlers can be associated with the nodes of a scene graph, allowing you to change the state of a node and its associated subgraph.

All global operations on the graph of the scene related to obtaining the final result on the screen are performed by the engine automatically, by periodically traversing the graph in depth.

ConsideredThe last time example was our scene consisted of a single object - an airplane model loaded from a file. Running very far forward, I will say that this model is a leaf node of the scene graph. It is tightly welded to the global base coordinate system of the engine.

2. Memory Management in OSG


Since the nodes of the scene graph store a lot of data about the scene objects and operations on them, it is necessary to allocate memory, including dynamically, to store this data. In this case, when manipulating the scene graph, and, for example, removing some of its nodes, you need to carefully monitor that the remote nodes of the graph are no longer processed. This process is always accompanied by errors, time-consuming debugging, since it is rather difficult for a developer to track which pointers to objects refer to existing data and which must be deleted. Without effective memory management, segmentation errors and memory leaks are likely.

Memory management is a critical task in OSG and its concept is based on two theses:

  1. Memory Allocation: Ensuring the allocation of storage space required by an object.
  2. Freeing memory: Returning the allocated memory to the system at that moment when it is not necessary.

Many modern programming languages, such as C #, Java, Visual Basic .Net, and the like, use the so-called garbage collector to free allocated memory. The concept of the C ++ language does not provide for a similar approach, but we can simulate it by using so-called smart pointers.

Today C ++ has smart pointers in its arsenal, which is called “out of the box” (and the C ++ standard 17 already managed to rid the language of some obsolete types of smart pointers), but this was not always the case. The earliest of the official versions of OSG number 0.9 was born in 2002, and before the first official release was still three years. At that time, the C ++ standard did not yet provide smart pointers, and if you believe one historical excursion, the language itself was going through hard times. So the appearance of a bicycle in the form of its own smart pointers, which are implemented in OSG is not surprising. This mechanism is deeply integrated into the structure of the engine, so it is absolutely necessary to understand its work from the very beginning.

3. Classes osg :: ref_ptr <> and osg :: Referenced


OSG provides its own smart pointer engine based on the osg :: ref_ptr <> template class to implement automatic garbage collection. For its proper operation, OSG provides another class osg :: Referenced for managing memory blocks for which reference counts are made.

The class osg :: ref_ptr <> provides several operators and methods.

  • get () is a public method that returns a “raw” pointer, for example, when using the template osg :: Node as an argument, this method returns osg :: Node *.
  • operator * () is actually a dereference operator.
  • operator -> () and operator = () allow using osg :: ref_ptr <> as a classic pointer when accessing methods and properties of objects described by this pointer.
  • operator == (), operator! = () and operator! () - allow you to perform comparison operations on smart pointers.
  • valid () is a public method that returns true if the managed pointer has a valid value (not NULL). The expression some_ptr.valid () is equivalent to the expression some_ptr! = NULL, if some_ptr is a smart pointer.
  • release () is a public method, useful when you want to return a managed address from a function. About him will be discussed in more detail later.

The osg :: Referenced class is the base class for all elements of the scene graph, such as nodes, geometry, rendering states, and other objects placed on the scene. Thus, by creating the root node of the scene, we indirectly inherit all the functionality provided by the class osg :: Referenced. Therefore, in our program there is an announcement

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

The class osg :: Referenced contains an integer counter of references to the allocated block of memory. This counter is initialized to zero in the class constructor. It is incremented by one when an osg :: ref_ptr <> object is created. This counter decreases as soon as any reference to the object described by this pointer is deleted. The object is automatically destroyed when it is no longer referenced by any smart pointers.

The osg :: Referenced class has three public methods:

  • ref () is a public method that increments the reference count by 1.
  • unref () is a public method that reduces the reference count by 1.
  • referenceCount () is a public method that returns the current value of the reference count, which is useful when debugging code.

These methods are available in all classes derived from osg :: Referenced. However, it should be remembered that manual control of the reference count can lead to unpredictable consequences, and taking advantage of this, you should be clear about what you are doing.

4. How OSG performs garbage collection and why it is needed


There are several reasons why smart pointers and garbage collection should be used:

  • Critical error minimization: the use of smart pointers automates the allocation and freeing of memory. There are no dangerous "raw" pointers.
  • Effective memory management: memory allocated for an object is released as soon as the object is no longer needed, which leads to economical use of system resources.
  • Facilitating debugging of an application: having the ability to clearly track the number of references to an object, we have opportunities for all sorts of optimizations and experiments.

Suppose that a scene graph consists of a root node and several levels of child nodes. If the root node and all child nodes are managed using the class osg :: ref_ptr <>, then the application can only track the pointer to the root node. Deleting this node will result in a sequential, automatic deletion of all child nodes.



Smart pointers can be used as local variables, global variables, class members and automatically reduce the reference count when the smart pointer goes out of scope.

Smart pointers are strongly recommended by OSG developers for use in projects, but there are a few key points to consider:

  • Instances of osg :: Referenced and its derivatives can be created exclusively on the heap. They cannot be created on the stack as local variables, since the destructors of these classes are declared as proteced. for example

osg::ref_ptr<osg::Node> node = new osg::Node; // правильно
osg::Node node; // неправильно

  • You can create temporary scene nodes using ordinary C ++ pointers, but this approach will be insecure. Better to use smart pointers to ensure correct control of the scene graph.

osg::Node *tmpNode = new osg::Node; // в принципе, будет работать...
osg::ref_ptr<osg::Node> node = tmpNode; // но лучше завершить работу с временным указателем таким образом!

  • In no case should cyclic references be used in the scene tree when a node references itself directly or indirectly through several levels.



In the above example, the scene graph of a Child 1.1 node is self-referencing, and the Child 2.2 node also references the Child 1.2 node. Such links can lead to incorrect calculation of the number of links and undefined program behavior.

5. Tracking managed objects


To illustrate the operation of the smart pointer mechanism in OSG, we will write the following synthetic example

main.h

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

main.cpp

#include"main.h"classMonitoringTarget :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;
};
intmain(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;
    return0;
}

Create a descendant class osg :: Referenced that does nothing except in the constructor and destructor informing that its instance has been created and displaying the identifier defined when creating the instance. Create a class instance using the smart pointer engine

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

Next, display the reference count for the target object.

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

After that, create a new smart pointer, assigning it the value of the previous pointer

osg::ref_ptr<MonitoringTarget> anotherTarget = target;

and again display the reference count

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

Let's see what we did by analyzing the output of the program.

15:42:39: Отладка запущена
Constructing target 0
Referenced count before referring: 1
Referenced count after referring: 2
Dsetroying target 0
15:42:42: Отладка завершена

When the class constructor is started, a corresponding message is displayed, telling us that the memory for the object is allocated and the constructor has worked normally. Further, after creating a smart pointer, we see that the reference count for the created object has increased by one. Creating a new pointer, assigning it the value of the old pointer is essentially creating a new link to the same object, so the reference count is increased by one more. When you exit the program, the MonitoringTarget class destructor is called.



Let's conduct one more experiment, adding such code to the end of the main () function

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

leading to such an "exhaust" program

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: Отладка завершена

We create several objects in the loop body using a smart pointer. Since the scope of the pointer in this case extends only to the body of the loop, when you exit it, the destructor is automatically called. This would not have happened, obviously, if we used ordinary pointers.

Another important feature of working with smart pointers is associated with automatic memory freeing. Since the destructor of the classes derived from osg :: Referenced is protected, we cannot explicitly call the delete operator to delete an object. The only way to delete an object is to nullify the number of links to it. But then our code becomes unsafe with multi-threaded data processing - we can access an already deleted object from another thread.

Fortunately, OSG provides a solution to this problem with its scheduler for deleting objects. This scheduler is based on the use of the class osg :: DeleteHandler. It works in such a way that it does not perform the operation of deleting an object right away, but executes it after a while. All objects to be deleted are temporarily remembered until a moment comes for safe removal, and then they are all deleted at once. The osg :: DeleteHandler removal scheduler is managed by the OSG render backend.

6. Return from function


Add the following function to our example code.

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

and replace the call to the new operator in a loop with a call to this function.

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

Calling release () will reduce the number of references to the object to zero, but instead of deleting the memory, returns directly the actual pointer to the allocated memory. If this pointer is assigned to another smart pointer, then there will be no memory leaks.

findings


The concepts of the scene graph and smart pointers are basic for understanding the principle of operation, and hence the effective use of OpenSceneGraph. With regard to OSG smart pointers, it should be remembered that their use is absolutely necessary when

  • It is assumed long-term storage facility
  • One object stores a link to another object.
  • It is necessary to return the pointer from the function

The sample code provided in the article is available here .

To be continued...

Also popular now: