Using the scene graph
Overview of scene management capabilities.
The scene graph provides a way to hierarchically manage your objects, their transformation, animation and rendering, among other things. The library is contained in the 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, a scene graph in Magnum is composed of three main components:
- objects, providing parent/children hierarchy
- transformations, implementing particular transformation type
- features, providing rendering capabilities, audio, animation, physics etc.
Basic concepts
The basic organization of a scene graph is as follows: a top-level scene object contains a hierarchy of objects . Each object has a transformation relative to its parent — usually a transformation matrix. The whole scene is rendered using a camera with a projection matrix . The projection matrix defines things like field-of-view, aspect ratio and near/far clipping planes. The final projective object transform , relative to camera, is calculated as a combination of all relative transformations up to the scene root (an absolute transformation), multiplied by the inverse of the camera's absolute transformation. For the object its final transform is produced as follows:
The inverse camera transformation is called a camera matrix. It's useful for example to calculate light positions relative to a camera.
The objects themselves handle only parent/child relationship and transformation. Features add behavior to them. The camera is one of them, together with a drawable . A drawable makes it possible to draw things on screen using a camera. It's not possible to just "draw the graph", the drawables are grouped into a drawable group . You can have just one, drawing everything at once, or group the drawables by a shader / transparency etc. It's also possible to have multiple cameras and switch among them.
Besides drawables, there are other features for animation, audio, physics, etc.
Transformations
A transformation handles object position, rotation, etc. Its basic property is a dimension count (2D or 3D) and an underlying numeric type. All classes in SceneGraph are templated on the 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 various transformation implementations for both 2D and 3D. Each implementation has its own advantages and disadvantages — for example when using matrices you can have nearly arbitrary transformations, but composing transformations, calculating their inverse and accounting for floating-point drift is a rather costly operation. On the other hand, quaternions for example won't allow you to scale or shear objects, but have far better performance characteristics.
It's also possible to implement your own transformation class for specific needs, see the source of builtin transformation classes for more information.
Magnum provides the following transformation classes. See documentation of each class for more detailed information:
- SceneGraph::
MatrixTransformation2D — arbitrary 2D transformations but with slow inverse transformations and no floating-point drift reduction - SceneGraph::
MatrixTransformation3D — arbitrary 3D transformations but with slow inverse transformations and no floating-point drift reduction - SceneGraph::
RigidMatrixTransformation2D — 2D translation, rotation and reflection (no scaling), with relatively fast inverse transformations and floating-point drift reduction - SceneGraph::
RigidMatrixTransformation3D — 3D translation, rotation and reflection (no scaling), with relatively fast inverse transformations and floating-point drift reduction - SceneGraph::
DualComplexTransformation — 2D translation and rotation with fast inverse transformations and floating-point drift reduction - SceneGraph::
DualQuaternionTransformation — 3D translation and rotation with fast inverse transformation and floating-point drift reduction - SceneGraph::
TranslationRotationScalingTransformation2D — 2D transformations with separate translation, rotation and scaling. Usable for scenes with animation tracks that control these properties separately. - SceneGraph::
TranslationRotationScalingTransformation3D — 3D transformations with separate translation, rotation and scaling. Usable for scenes with animation tracks that control these properties separately. - SceneGraph::
TranslationTransformation*D — Just 2D/3D translation (no rotation, scaling or anything else)
Common usage of transformation classes is to typedef SceneGraph::
typedef SceneGraph::Scene<SceneGraph::MatrixTransformation3D> Scene3D; typedef SceneGraph::Object<SceneGraph::MatrixTransformation3D> Object3D;
The object type is subclassed from the transformation type and so the Object3D
type will then contain all members from both SceneGraph::
Scene3D scene; Object3D object; object.setParent(&scene) .rotateY(15.0_degf) .translate(Vector3::xAxis(5.0f));
Scene hierarchy
Scene hierarchy is an essential part of the scene graph. In the root there is a SceneGraph::
Build the hierarchy by parenting one object to another. Parent object can be either passed in the constructor or set using SceneGraph::
Scene3D scene; Object3D* first = new Object3D{&scene}; Object3D* second = new Object3D{first};
This hierarchy also takes care of memory management — when an object is destroyed, all its children are destroyed too. See detailed explanation of construction and destruction order below for information about possible issues. To reflect the implicit memory management in the code better, you can use SceneGraph::new
call from the code above:
Scene3D scene; Object3D& first = scene.addChild<Object3D>(); Object3D& second = first.addChild<Object3D>();
Object features
Magnum provides the following builtin features. See documentation of each class for more detailed information and usage examples:
- SceneGraph::
Camera*D — Handles projection matrix, aspect ratio correction etc.. Used for rendering parts of the scene. - SceneGraph::
Drawable*D — Adds drawing functionality to given object. Group of drawables can be then rendered using the camera feature. - Audio::
Listener*D — Handles audio listener properties like position and orientation. Audio equivalent of a camera. - Audio::
Playable*D — Handles audio source properties. Audio equivalent of a drawable. - SceneGraph::
Animable*D — Adds animation functionality to given object. Group of animables can be then controlled using SceneGraph:: AnimableGroup*D. - DebugTools::
ObjectRenderer*D, DebugTools:: ForceRenderer*D — Visualize object properties, object shape or force vector for debugging purposes. See Debugging helpers for more information.
Each feature takes a reference to holder object in its constructor, so adding a feature to an object might look just like the following, as in some cases you don't even need to keep the pointer to it. List of object features is accessible through SceneGraph::
Object3D o; new MyFeature{o, some, params};
Some features are passive, others active. Passive features can just be added to an object, with no additional work except for possible configuration (for example a debug renderer). Active features require the user to implement some virtual function (for example to draw the object on screen or perform an animation step). To make things convenient, features can be added directly to object itself using multiple inheritance, so you can add all the active features you want and implement functions you need in your own SceneGraph::
class BouncingBall: public Object3D, SceneGraph::Drawable3D, SceneGraph::Animable3D { public: explicit BouncingBall(Object3D* parent): Object3D{parent}, SceneGraph::Drawable3D{*this}, SceneGraph::Animable3D{*this} {} private: // drawing implementation for Drawable feature void draw(const Matrix4&, SceneGraph::Camera3D&) override; // animation step for Animable feature void animationStep(Float, Float) 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 the 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. Also, there is the addFeature() counterpart to addChild():
Object3D o; o.addFeature<MyFeature>(some, params);
Transformation caching in features
Some features need to operate with absolute transformations and their inversions — for example the camera needs its inverse transformation (camera matrix) 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 its transformation, its parent or by explicitly calling SceneGraph::
Usually you will need caching in the SceneGraph::
class CachingObject: public Object3D, SceneGraph::AbstractFeature3D { public: explicit CachingObject(Object3D* parent): 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 SceneGraph::
Polymorphic access to object transformation
Features by default have access only to SceneGraph::
To solve this, the transformation classes are subclassed from interfaces sharing common functionality, so the feature can use that interface instead of being specialized for all relevant transformation implementations. The following interfaces are available, each having its own set of virtual functions to control the transformation:
- SceneGraph::
AbstractTransformation*D — base for all transformations - SceneGraph::
AbstractTranslation*D — base for all transformations providing translation - SceneGraph::
AbstractTranslationRotation2D, SceneGraph:: AbstractTranslationRotation3D — base for all transformations providing translation and rotation - SceneGraph::
AbstractBasicTranslationRotationScaling2D, SceneGraph:: AbstractBasicTranslationRotationScaling3D — base for all transformations providing translation, rotation and scaling
These interfaces provide virtual functions which can be used to modify object transformations. The virtual calls are used only when calling through the interface and not when using the concrete implementation directly to avoid negative performance effects. There are no functions to retrieve object transformation, you need to use the above transformation caching mechanism for that.
In the following example we are able to get pointer to both the SceneGraph::
class TransformingFeature: public SceneGraph::AbstractFeature3D { public: template<class T> explicit TransformingFeature(SceneGraph::Object<T>& object): SceneGraph::AbstractFeature3D(object), _transformation(object) {} … private: SceneGraph::AbstractTranslationRotation3D& _transformation; };
If we take for example SceneGraph::
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 the 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 a member it's not an issue, however 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).