Magnum
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
Using scene graph

Table of Contents

Overview of scene management capabilities.

Scene graph provides way to hiearchically manage your objects, their transformation, physics interaction, animation and rendering. The library is contained in SceneGraph namespace, see its documentation for more information about building and usage with CMake.

There are naturally many possible feature combinations (2D vs. 3D, different transformation representations, animated vs. static, object can have collision shape, participate in physics events, have forward vs. deferred rendering...) and to make everything possible without combinatiorial explosion and allow the users to provide their own features, scene graph in Magnum is composed of three main components:

Note
Fully contained applications with initial scene graph setup are available in scenegraph2D and scenegraph3D branches of Magnum Bootstrap repository.

Transformations

Transformation handles object position, rotation etc. and its basic property is dimension count (2D or 3D) and underlying floating-point type.

Note
All classes in SceneGraph are templated on underlying type. However, in most cases Float is used and thus nearly all classes have convenience aliases so you don't have to explicitly specify it.

Scene graph has implementation of transformations in both 2D and 3D, using either matrices or combination of position and rotation. Each implementation has its own advantages and disadvantages – for example when using matrices you can have nearly arbitrary transformations, but composing transformations and computing their inverse is costly operation. On the other hand quaternions won't allow you to scale or shear objects, but are more memory efficient than matrices.

It's also possible to implement your own transformation class for specific needs, see source of other transformation classes for more information.

Scene hierarchy

Scene hierarchy is skeleton part of scene graph. In the root there is Scene and its children are Object instances. The hierarchy has some transformation type, identical for all objects (because for example having part of the tree in 2D and part in 3D just wouldn't make sense). Common usage is to typedef Scene and Object with desired transformation type to save unnecessary typing later:

typedef SceneGraph::Scene<SceneGraph::MatrixTransformation3D> Scene3D;
typedef SceneGraph::Object<SceneGraph::MatrixTransformation3D> Object3D;

Then you can start building the hierarchy by parenting one object to another. Parent object can be either passed in constructor or using Object::setParent(). Scene is always root object, so it naturally cannot have parent object.

Scene3D scene;
auto first = new Object3D(&scene);
auto second = new Object3D(first);

Object children can be accessed using Object::firstChild() and Object::lastChild(), then you can traverse siblings (objects with the same parent) with Object::previousSibling() and Object::nextSibling(). For example all children of an object can be traversed the following way:

Object3D* o;
for(Object3D* child = o->firstChild(); child; child = child->nextSibling()) {
// ...
}

The hierarchy takes care of memory management - when an object is destroyed, all its children are destroyed too. See detailed explanation of construction and destruction order for information about possible issues.

The object is derived from the transformation you specified earlier in the typedef, so you can directly transform the objects using methods of given transformation implementation. Scene, as a root object, cannot have any transformation. For convenience you can use method chaining:

auto next = new Object3D;
next->setParent(another)
.translate(Vector3::yAxis(3.0f))
.rotateY(35.0_degf);

Object features

The object itself handles only parent/child relationship and transformation. To make the object renderable, animatable, add collision shape to it etc., you have to add a feature to it.

Each feature takes reference to holder object in constructor, so adding a feature to an object might look just like this, as in some cases you don't even need to keep the pointer to it:

Object3D* o;
new MyFeature(o);

Features of an object can be accessed using Object::firstFeature() and Object::lastFeature(), then you can traverse the features using AbstractFeature::previousFeature() and AbstractFeature::nextFeature(), similarly to traversing object children:

Object3D* o;
for(Object3D::FeatureType feature = o->firstFeature(); feature; feature = feature->nextFeature()) {
// ...
}

Some features are passive, some active. Passive features can be just added to an object like above, without any additional work (for example collision shape). Active features require the user to implement some virtual function (for example to draw the object on screen or perform animation step). To make things convenient, features can be added directly to object itself using multiple inheritance, so you can conveniently add all the active features you want and implement needed functions in your own Object subclass without having to subclass each feature individually (and making the code overly verbose). Simplified example:

class Bomb: public Object3D, SceneGraph::Drawable3D, SceneGraph::Animable3D {
public:
Bomb(Object3D* parent): Object3D(parent), SceneGraph::Drawable3D(*this), SceneGraph::Animable3D(*this) {}
protected:
// drawing implementation for Drawable feature
void draw(...) override;
// animation step for Animable feature
void animationStep(...) override;
};

From the outside there is no difference between features added "at runtime" and features added using multiple inheritance, they can be both accessed from feature list.

Similarly to object hierarchy, when destroying object, all its features (both member and inherited) are destroyed. See detailed explanation of construction and destruction order for information about possible issues.

Transformation caching

Some features need to operate with absolute transformations and their inversions - for example camera needs its inverse transformation to render the scene, collision detection needs to know about positions of surrounding objects etc. To avoid computing the transformations from scratch every time, the feature can cache them.

The cached data stay until the object is marked as dirty - that is by changing transformation, changing parent or explicitly calling Object::setDirty(). If the object is marked as dirty, all its children are marked as dirty too and AbstractFeature::markDirty() is called on every feature. Calling Object::setClean() cleans the dirty object and all its dirty parents. The function goes through all object features and calls AbstractFeature::clean() or AbstractFeature::cleanInverted() depending on which caching is enabled on given feature. If the object is already clean, Object::setClean() does nothing.

Most probably you will need caching in Object itself – which doesn't support it on its own – however you can take advantage of multiple inheritance and implement it using AbstractFeature. In order to have caching, you must enable it first, because by default the caching is disabled. You can enable it using AbstractFeature::setCachedTransformations() and then implement corresponding cleaning function(s):

class CachingObject: public Object3D, SceneGraph::AbstractFeature3D {
public:
CachingObject(Object3D* parent): SceneGraph::AbstractFeature3D(*this) {
setCachedTransformations(SceneGraph::CachedTransformation::Absolute);
}
protected:
void clean(const Matrix4& absoluteTransformation) override {
absolutePosition = absoluteTransformation.translation();
}
private:
Vector3 absolutePosition;
};

When you need to use the cached value, you can explicitly request the cleanup by calling Object::setClean(). Camera, for example, calls it automatically before it starts rendering, as it needs its own inverse transformation to properly draw the objects.

See SceneGraph-AbstractFeature-subclassing-caching for more information.

Construction and destruction order

There aren't any limitations and usage trade-offs of what you can and can't do when working with objects and features, but there are two issues which you should be aware of:

Object hierarchy

When objects are created on the heap (the preferred way, using new), they can be constructed in any order and they will be destroyed when their parent is destroyed. When creating them on the stack, however, they will be destroyed when they go out of scope. Normally, the natural order of creation is not a problem:

{
Scene3D scene;
Object3D object(&scene);
}

The object is created last, so it will be destroyed first, removing itself from scene's children list, causing no problems when destroying scene object later. However, if their order is swapped, it will cause problems:

{
Object3D object;
Scene3D scene;
object.setParent(&scene);
} // crash!

The scene will be destroyed first, deleting all its children, which is wrong, because object is created on stack. If this doesn't already crash, the object destructor is called (again), making things even worse.

Member and inherited features

When destroying the object, all its features are destroyed. For features added as member it's no issue, features added using multiple inheritance must be inherited after the Object class:

class MyObject: public Object3D, MyFeature {
public:
MyObject(Object3D* parent): Object3D(parent), MyFeature(*this) {}
};

When constructing MyObject, Object3D constructor is called first and then MyFeature constructor adds itself to Object3D's list of features. When destroying MyObject, its destructor is called and then the destructors of ancestor classes – first MyFeature destructor, which will remove itself from Object3D's list, then Object3D destructor.

However, if we would inherit MyFeature first, it will cause problems:

class MyObject: MyFeature, public Object3D {
public:
MyObject(Object3D* parent): MyFeature(*this), Object3D(parent) {} // crash!
};

MyFeature tries to add itself to feature list in not-yet-constructed Object3D, causing undefined behavior. Then, if this doesn't already crash, Object3D is created, creating empty feature list, making the feature invisible.

If we would construct them in swapped order (if it is even possible), it wouldn't help either:

class MyObject: MyFeature, public Object3D {
public:
MyObject(Object3D* parent): Object3D(parent), MyFeature(*this) {}
// crash on destruction!
};

On destruction, Object3D destructor is called first, deleting MyFeature, which is wrong, because MyFeature is in the same object. After that (if the program didn't already crash) destructor of MyFeature is called (again).