





















































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.
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.
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} )
endmacro()
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 )
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.
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.
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.
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.
osg::ref_ptr<osg::Node> root = osgDB::readNodeFile("cessna.osg"); osgViewer::Viewer viewer; viewer.setSceneData( root.get() ); return viewer.run();
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.
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:
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.
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:
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:
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.
Here are some reasons for using smart pointers and the garbage collection system in programming:
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::ref_ptr<osg::Node> node = new osg::Node; // this is legal osg::Node node; // this is illegal!
osg::Node* tmpNode = new osg::Node; // this is OK ... osg::ref_ptr<osg::Node> node = tmpNode; // Good finish!
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:
Now let's have a better grasp of the basic concepts of memory management, through a very simple example.