How to Create an OpenSceneGraph Application

Rui Wang

April 2011


OpenSceneGraph 3.0: Beginner's Guide

OpenSceneGraph 3.0: Beginner's Guide

Create high-performance virtual reality applications with OpenSceneGraph, one of the best 3D graphics engines.

        Read more about this book      

(For more resources on OpenSceneGraph, see here.)

Constructing your own projects

To build an executable program from your own source code, a platform-dependent solution or makefile is always required.

At the beginning of this article, we are going to introduce another way to construct platform-independent projects with the CMake system, by which means, we are able to focus on interacting with the code and ignore the painstaking compiling and building process.

Time for action – building applications with CMake

Before constructing your own project with CMake scripts, it could be helpful to keep the headers and source files together in an empty directory first. The second step is to create a CMakeLists.txt file using any text editor, then and start writing some simple CMake build rules.

  1. The following code will implement a project with additional OSG headers and dependency libraries. Please enter them into the newly-created CMakeLists.txt file:

    cmake_minimum_required( VERSION 2.6 )
    project( MyProject )

    find_package( OpenThreads )
    find_package( osg )
    find_package( osgDB )
    find_package( osgUtil )
    find_package( osgViewer )

    macro( config_project PROJNAME LIBNAME )
    include_directories( ${${LIBNAME}_INCLUDE_DIR} )
    target_link_libraries( ${PROJNAME} ${${LIBNAME}_LIBRARY} )

    add_executable( MyProject main.cpp )
    config_project( MyProject OPENTHREADS )
    config_project( MyProject OSG )
    config_project( MyProject OSGDB )
    config_project( MyProject OSGUTIL )
    config_project( MyProject OSGVIEWER )

  2. We have only added a main.cpp source file here, which is made up of the "Hello World" example and will be compiled to generate an executable file named MyProject. This small project depends on five major OSG components. All of these configurations can be modified to meet certain requirements and different user applications.
  3. Next, start cmake-gui and drag your CMakeLists.txt into the GUI. You may not be familiar with the CMake scripts to be executed, at present. However, the CMake wiki will be helpful for further understanding:
  4. Create and build a Visual Studio solution or a makefile.
  5. The only point is that you have to ensure that your CMake software version is equal to or greater than 2.6, and make sure you have the OSG_ROOT environment variable set. Otherwise, the find_package() macro may not be able to find OSG installations correctly. The following image shows the unexpected errors encountered because OSG headers and libraries were not found in the path indicated by OSG_ROOT (or the variable was just missed):

    OpenSceneGraph 3.0: Beginner's Guide

  6. Note that, there is no INSTALL project in the Visual Studio solution, or any make install command to run at this time, because we don't write such CMake scripts for post-build installations. You could just run the executable file in the build directory directly.

What just happened?

CMake provides easy-to-read commands to automatically find dependencies for user projects. It will check preset directories and environment variables to see if there are any headers and libraries for the required package.

The environment variable OSG_ROOT (OSG_DIR is OK, too) will facilitate in looking for OSG under Windows and UNIX, as CMake will first search for valid paths defined in it, and check if there are OSG prebuilt headers and libraries existing in these paths.

Have a go hero – testing with different generators

Just try a series of tests to generate your project, using Visual Studio, MinGW, and the UNIX gcc compiler. You will find that CMake is a convenient tool for building binary files from source code on different platforms. Maybe this is also a good start to learning programming in a multi-platform style.

Using a root node

Now we are going to write some code and build it with a self-created CMake script. We will again make a slight change to the frequently-used "Hello World" example.

Time for action – improving the "Hello World" example

The included headers, <osgDB/ReadFile> and <osgViewer/Viewer>, do not need to be modified. We only add a root variable that provides the runtime access to the Cessna model and assigns it to the setSceneData() method.

  1. In the main entry, record the Cessna model with a variable named root:
    osg::ref_ptr<osg::Node> root = osgDB::readNodeFile("cessna.osg");
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
  2. Build and run it at once:

    OpenSceneGraph 3.0: Beginner's Guide

  3. You will see no difference between this example and the previous "Hello World". So what actually happened?

What just happened?

In this example, we introduced two new OSG classes: osg::ref_ptr<> and osg::Node. The osg::Node class represents the basic element of a scene graph. The variable root stands for the root node of a Cessna model, which is used as the scene data to be visualized.

Meanwhile, an instance of the osg::ref_ptr<> class template is created to manage the node object. It is a smart pointer, which provides additional features for the purpose of efficient memory management.

Understanding memory management

In a typical programming scenario, the developer should create a pointer to the root node, which directly or indirectly manages all other child nodes of the scene graph. In that case, the application will traverse the scene graph and delete each node and its internal data carefully when they no longer need to be rendered. This process is tiresome and error-prone, debugging dozens of bad trees and wild pointers, because developers can never know how many other objects still keep a pointer to the one being deleted. However without writing the management code, data segments occupied by all scene nodes will never be deleted, which will lead to unexpected memory leaks.

This is why memory management is important in OSG programming. A basic concept of memory management always involves two topics:

  1. Allocation: Providing the memory needed by an object, by allocating the required memory block.
  2. Deallocation: Recycling the allocated memory for reuse, when its data is no longer used.

Some modern languages, such as C#, Java, and Visual Basic, use a garbage collector to free memory blocks that are unreachable from any program variables. That means to store the number of objects reaching a memory block, and deallocate the memory when the number decrements to zero.

The standard C++ approach does not work in such a way, but we can mimic it by means of a smart pointer, which is defined as an object that acts like a pointer, but is much smarter in the management of memory. For example, the boost library provides the boost::shared_ptr<> class template to store pointers in order to dynamically allocated related objects.

ref_ptr<> and Referenced classes

Fortunately, OSG also provides a native smart pointer, osg::ref_ptr<>, for the purpose of automatic garbage collection and deallocation. To make it work properly, OSG also provides the osg::Referenced class to manage reference-counted memory blocks, which is used as the base class of any classes that may serve as the template argument.

The osg::ref_ptr<> class template re-implements a number of C++ operators as well as member functions, and thus provides convenient methods to developers. Its main components are as follows:

  • get(): This public method returns the managed pointer, for instance, the osg::Node* pointer if you are using osg::Node as the template argument.
  • operator*(): This is actually a dereference operator, which returns l-value at the pointer address, for instance, the osg::Node& reference variable.
  • operator->() and operator=(): These operators allow a user application to use osg::ref_ptr<> as a normal pointer. The former calls member functions of the managed object, and the latter replaces the current managed pointer with a new one.
  • operator==(), operator!=(), and operator!(): These operators help to compare smart pointers, or check if a certain pointer is invalid. An osg::ref_ptr<> object with NULL value assigned or without any assignment is considered invalid.
  • valid(): This public method returns true if the managed pointer is not NULL. The expression some_ptr.valid() equals to some_ptr!=NULL if some_ptr is defined as a smart pointer.
  • release(): This public method is useful when returning the managed address from a function.

The osg::Referenced class is the pure base class of all elements in a scene graph, such as nodes, geometries, rendering states, and any other allocatable scene objects. The osg::Node class actually inherits from osg::Referenced indirectly. This is the reason why we program as follows:

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

The osg::Referenced class contains an integer number to handle the memory block allocated. The reference count is initialized to 0 in the class constructor, and will be increased by 1 if the osg::Referenced object is referred to by an osg::ref_ptr<> smart pointer. On the contrary, the number will be decreased by 1 if the object is removed from a certain smart pointer. The object itself will be automatically destroyed when no longer referenced by any smart pointers.

The osg::Referenced class provides three main member methods:

  • The public method ref() increases the referenced counting number by 1
  • The public method unref() decreases the referenced counting number by 1
  • The public method referenceCount() returns the value of the current referenced counting number, which is useful for code debugging

These methods could also work for classes that are derived from osg::Referenced. Note that it is very rarely necessary to call ref() or unref() directly in user programs, which means that the reference count is managed manually and may conflict with what the osg::ref_ptr<> is going to do. Otherwise, OSG's internal garbage collecting system will get the wrong number of smart pointers in use and even crash when managing memory blocks in an improper way.

Collecting garbage: why and how

Here are some reasons for using smart pointers and the garbage collection system in programming:

  • Fewer bugs: Using smart pointers means the automatic initialization and cleanup of pointers. No dangling pointers will be created because they are always reference-counted.
  • Efficient management: Objects will be reclaimed as soon as they are no longer referenced, which gives more available memory to applications with limited resources.
  • Easy to debug: We can easily obtain the referenced counting number and other information on objects, and then apply other optimizations and experiments.

For instance, a scene graph tree is composed by a root node and multiple levels of child nodes. Assuming that all children are managed with osg::ref_ptr<>, user applications may only keep the pointer to the root node. As is illustrated by the following image, the operation of deleting the root node pointer will cause a cascading effect that will destroy the whole node hierarchy:

Each node in the example scene graph is managed by its parent, and will automatically be unreferenced during the deletion of the parent node. This node, if no longer referenced by any other nodes, will be destroyed immediately, and all of its children will be freed up. The entire scene graph will finally be cleaned without worries after the last group node or leaf node is deleted.

The process is really convenient and efficient, isn't it? Please make sure the OSG smart pointer can work for you, and use a class derived from osg::Referenced as the osg::ref_ptr<> template argument, and correctly assign newly-allocated objects to smart pointers.

A smart pointer can be used either as a local variable, a global variable, or a class member variable, and will automatically decrease the referenced counting number when reassigned to another object or moved out of the smart pointer's declaration scope.

It is strongly recommended that user applications always use smart pointers to manage their scenes, but there are still some issues that need special attention:

  • osg::Referenced and its derivatives should be created from the heap only. They cannot be used as local variables because class destructors are declared protected internally for safety. For example:
    osg::ref_ptr<osg::Node> node = new osg::Node; // this is legal
    osg::Node node; // this is illegal!
  • A regular C++ pointer is still workable temporarily. But user applications should remember to assign it to osg::ref_ptr<> or add it to a scene graph element (almost all OSG scene classes use smart pointers to manage child objects) in the end, as it is always the safest approach.
    osg::Node* tmpNode = new osg::Node; // this is OK
    osg::ref_ptr<osg::Node> node = tmpNode; // Good finish!
  • Don't play with reference cycles, as the garbage collecting mechanism cannot handle it. A reference cycle means that an object refers to itself directly or indirectly, which leads to an incorrect calculation of the referenced counting number.

The scene graph shown in the following image contains two kinds of reference cycles, which are both invalid. The node Child 1.1 directly adds itself as the child node and will form a dead cycle while traversing to its children, because it is the child of itself, too! The node Child 2.2, which also makes a reference cycle indirectly, will cause the same problem while running:

OpenSceneGraph 3.0: Beginner's Guide

Now let's have a better grasp of the basic concepts of memory management, through a very simple example.



        Read more about this book      

(For more resources on OpenSceneGraph, see here.)

Tracing the managed entities

The main point that we are interested in is how osg::ref_ptr<> binds and handles an osg::Referenced object, and when the managed object will be destroyed. What we have already learnt is: the managed object will be automatically destroyed when it is no longer referenced by any smart pointers, or when its referrer is out of the declaration scope. Now let's see how this is performed in practice.

Time for action – monitoring counted objects

We will first declare a customized class that is derived from osg::Referenced. This can benefit from the garbage collecting system by using the smart pointer. After that, let's take a look at the initialization and cleanup procedures of our referenced objects.

  1. Include the necessary headers:
    #include <osg/ref_ptr>
    #include <osg/Referenced>
    #include <iostream>
  2. Define the customized MonitoringTarget class with a unique name, _id. We will simply use the standard output to print out verbose information when constructing and destructing:

    class MonitoringTarget : public osg::Referenced
    MonitoringTarget( int id ) : _id(id)
    { std::cout << "Constructing target " << _id << std::endl; }

    virtual ~MonitoringTarget()
    { std::cout << "Destroying target " << _id << std::endl; }

    int _id;

  3. In the main function, we will first create a new MonitoringTarget object, and assign it to two different smart pointers, target and anotherTarget, and see if the referenced count changed:

    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;

  4. A second experiment is to create new objects in a cycle, but never delete them. Do you think this will cause memory leaks or not?

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

  5. The result is printed as shown in the following screenshot. As the construction and destruction processes both write to the standard output, a list of texts will be produced in the console.

    OpenSceneGraph 3.0: Beginner's Guide

What just happened?

A new MonitoringTarget object was created with the ID 0 and assigned to the smart pointer target. Another smart pointer, anotherTarget, immediately refers to the target and thus increases the referenced count of the MonitoringTarget object to 2, which means that the object is referenced by two smart pointers at the same time. It won't be deleted until all referrers are redirected or destroyed, as illustrated:

OpenSceneGraph 3.0: Beginner's Guide

After that, we were going to try constructing MonitoringTarget objects with the ID 1 to 4 in a cycle. Every time, the allocated object was set to an osg::ref_ptr<> pointer, but without any explicit deletion. You will notice that the MonitoringTarget object was automatically deleted at the end of each loop, and would never cause memory leaks.

Another interesting issue is to decide the best time to actually delete an unreferenced object. Most osg::Referenced-based classes define their destructors as protected members, so the C++ delete operator can't be used directly in user programs. The deletion process will be performed internally when the reference count decreases to 0. But this may still cause serious problems if some other threads are working on the object at the same time when it is being deleted. That is to say, the garbage collecting system may not be thread-safe for massive use!

Fortunately, OSG has already provided an object deletion scheduler in response to the problem. This deletion scheduler, named osg::DeleteHandler, will not perform the deleting operation at once, but defer it for a while. All objects to be deleted will be stored temporarily, until it is a safe time to release them. The osg::DeleteHandler class is managed by the OSG rendering backend. User applications should always pay little attention to this class, unless you have to implement another deletion handler yourselves, some day.

Have a go hero – returning from a function

We have already mentioned that there is a release() method that can be used when returning from a function. The following code will tell more about its usage:

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

Try replacing the new MonitoringTarget(i) statements in the last example with this function. It is for the purpose of returning from a function and has no side effects.

Parsing command-line arguments

Command-line arguments to the main function define different parameters for user applications. The main function declaration always looks like this:

int main( int argc, char** argv );

The argc and argv arguments form a string array containing the application name and other necessary arguments. OSG provides a fast and safe osg::ArgumentParser to read and make use of them.

Time for action – reading the model filename from the command line

The most common public method of osg::ArgumentParser is the overloaded read() function. In this example, we are going to read command-line arguments with a special format and apply the parsing result to the osgDB::readNodeFile() function.

  1. Include the necessary headers:
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
  2. In the main function, try reading --model and the filename from the input arguments:
    osg::ArgumentParser arguments( &argc, argv );
    std::string filename; "--model", filename );
  3. Read Node from the specified file and initialize the viewer. This is very similar to some previous examples except that it replaces the const string "Cessna.osg" with a std::string variable:
    osg::ref_ptr<osg::Node> root = osgDB::readNodeFile( filename );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
  4. Build and start this example! Assuming that your executable file is MyProject.exe, type the following command in the prompt:
    # MyProject.exe --model dumptruck.osg
  5. We will see more than a Cessna model now. It is a dump truck loaded from the disk! Please be aware that you should have the OSG sample data installed, and the environment variable OSG_FILE_PATH set.

What just happened?

The dump truck model is loaded and rendered on the screen. Here, the most important point is that the filename dumptruck.osg is obtained from the command-line argument. The read() function, which consists of a format string parameter and a result parameter, helps to successfully find the first occurrence of the user-defined option --model and the filename argument that follows.

The read() function of the osg::ArgumentParser class is overloaded. You may obtain integers, float and double values, and even mathematical vectors, in addition to strings, from its parameters. For instance, to read a customized option --size with a single precision value from the command line, just use the following code:

float size = 0.0f; "--size", size );

The initial value of size will not be changed if there is no such argument, --size.

Tracing with the notifier

The OSG notifier mechanism provides a novel method of outputting verbose debugging messages, either from the OSG rendering backend or from the user level. It is really an important and time-honored method for tracing and debugging programs. In addition, the notifier is also used throughout the OSG core functionalities and plugins to show errors, warning messages, or information about the work in progress. Developers may simply insert debugging print functions liberally in the source code files. The print function, osg::notify(), is designed to accept different levels of messages and send them to the console or user-defined controllers.

The osg::notify() function can be used as the standard output stream std::cout. It requires a NotifySeverity argument to indicate the message level, which can be ALWAYS, FATAL, WARN, NOTICE, INFO, DEBUG_INFO, and DEBUG_FP, sorted from the most severity to the least. For instance:

osg::notify(osg::WARN) << "Some warn message." << std::endl;

This will print out a line of the warning message by default. Here osg::WARN is used to indicate the notify level to the OSG notifier system.

A series of macro definitions, such as OSG_FATAL, OSG_WARN, and OSG_NOTICE, will do the same work as the osg::notify() function, with different severity levels.

Redirecting the notifier

The OSG output message always includes important information about the running state, graphics system extensions, and possible problems in the OSG backend and user applications. This is also important as a reference source for debugging OSG-based programs.

In some cases, there is no console output in an application, which prevents us from reading notifier messages and finding possible bugs. However, the osg::NotifyHandler derived class can be used to redirect the notifier to another output stream, such as files or GUI widgets.

Time for action – saving the log file

We will make use of the std::ofstream class to redirect the OSG internal notify messages to an external log file. The virtual function notify() of the osg::NotifyHandler derived class should be overridden to apply standard file stream operations, and a global function osg::setNotifyHandler() is called before everything starts as well.

  1. Include the necessary headers:
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    #include <fstream>
  2. Implement the derived class LogFileHandler, which will redirect notify messages to the file stream:

    class LogFileHandler : public osg::NotifyHandler
    LogFileHandler( const std::string& file )
    { file.c_str() ); }
    virtual ~LogFileHandler() { _log.close(); }

    virtual void notify(osg::NotifySeverity severity,
    const char* msg)
    { _log << msg; }

    std::ofstream _log;

  3. Now set a new notify handler to the entire OSG system, and work under the INFO level to see more verbose messages. The function osgDB::readNodeFiles here directly reads all usable filenames from the command line and merges them into the root node. We also add an OSG_FATAL macro to check if there is no scene graph data loaded:

    int main( int argc, char** argv )
    osg::setNotifyLevel( osg::INFO );
    osg::setNotifyHandler( new LogFileHandler("output.txt") );

    osg::ArgumentParser arguments( &argc, argv );
    osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(
    arguments );
    if ( !root )
    OSG_FATAL << arguments.getApplicationName()
    <<": No data loaded." << std::endl;
    return -1;

    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );

  4. Build and start the example. All information will be saved in the log file output.txt, which is also indicated in the example. Try the command line with the newly-generated executable MyProject.exe this time:
    # MyProject.exe dumptruck.osg
  5. Press the Esc key to quit, and then open the resulting log file in the working directory with notepad (on Windows) or any text editor:

    (Move the mouse over the image to enlarge.)

  6. Don't be discouraged if you can't read and understand all of the information listed here. It only shows how OSG is starting and getting every part to work properly. It will be of great help in future development.

What just happened?

By default, OSG will send messages to the standard output stream std::cout and error stream std::cerr. However, these messages can be easily redirected to other streams and even the GUI text windows. A log file here is friendly to end users and helps them a lot, while sending feedbacks.

Besides, setting the osg::setNotifyLevel() function will make the notify level reset to the specified level or a higher level. The notifier system then ignores statements from lower levels and prints nothing to the output stream. For instance, assuming that you have the following lines in your application:

osg::setNotifyLevel( osg::FATAL );
osg::notify(osg::WARN) << "Some warn message." << std::endl;

The message with the notifier level lower than FATAL will not be printed any more.

The environment variable OSG_NOTIFY_LEVEL can be used to control the displayed message level, too, for any OSG-based applications.


This article provided a simple guide to creating your own simple OSG program with the CMake tool, and introduced some practical utilities. OSG uses smart pointers heavily for efficient manipulation of operating system resources allocated for each scene graph node at run time, which is crucial to the performance of these safety-critical programs. To help understand the working principle of smart pointers, we spent much of the article explaining the use of osg::ref_ptr<> and how to calculate the referenced count, and discussing various situations that may occur when managing OSG scene elements.

In this article, we specifically covered:

  • How to write a simple CMake script file and make it work with your own source code and OSG dependencies
  • The principle of smart pointers and the garbage collection mechanism of OSG
  • Advantages and notes on using the native smart pointers with scene graph objects
  • Some other useful classes and functions for parsing command-line arguments, and tracing and debugging your source code

Further resources on this subject:

You've been reading an excerpt of:

OpenSceneGraph 3.0: Beginner's Guide

Explore Title