Our rendering code is based on OpenSceneGraph, which is based on OpenGL. The best way to learn about OpenSceneGraph is to read its code and mailing list/forum, the mailing list/forum has many helpful explainers when you search for a particular topic.
Because OpenSceneGraph is a very powerful framework, and as they say "with great power comes great responsibility" - it is best to decide on some common usage practices, in order to avoid any problems.
Scene graph basics
The scene graph is made up of:
- StateSet: a collection of rendering states (e.g. Textures).
- Node: a node in the scene graph. A Node can have a StateSet.
- Group: a Node that has children Nodes.
- Drawable: a Node that contains drawing commands. The simplest implementation of a Drawable is the Geometry class, which consists of vertices, triangles and texture coordinates.
All of these classes are polymorphic and can also be customized by way of callbacks.
Please note some OSG examples still make use of a Geode class to attach Drawables, this is obsolete now as Drawables can be placed directly as a node in the scene graph. Geode should not be used.
In order to inspect or manipulate a scene graph, you can use a NodeVisitor.
Node masks are an important tool to control which parts of the scene graph are used for which purpose.
The frame loop
The rendering portion of the frame loop is structured into several phases. You can see these in action with the profiling panel brought up by the 'F3' key in-game.
The default threading model in OpenSceneGraph, DrawThreadPerContext, requires some care when dealing with modifications to rendering data (Drawables and StateSets). Any such data that is currently in (or has been in) the scene graph (referred to as 'live' data) must be assumed as unsafe to modify because it could be in use by the drawing thread.
What is not allowed:
- Modify a live StateSet (e.g. by adding/removing StateAttributes or modifying a contained StateAttribute)
- Modify a live Drawable in a way that affects its drawing implementation
What is allowed:
- Remove a Drawable from, or add it to the scene graph (the scene graph itself is not part of rendering data)
- (un)assign a StateSet from a node - the StateSets used for a rendering traversal are stored separately, so this is fine.
- Make harmless modifications that don't affect the renderer, like adding callbacks, changing names, etc. Still be very careful, though.
Whenever you are dealing with a bug that you think may be threading-related, you can use the OSG_THREADING=SingleThreaded environment variable to test your theory. As long as you stick to the above rules, though, you shouldn't run into any issues.
To make actual modifications, one of the following techniques may be used:
- Clone the original object and modify that copy. This is perfect for infrequent modifications when we don't care about the performance overhead of object cloning/deletion. Example
- Use a double buffering technique to manage access. We have a StateSetUpdater class that implements this technique for StateSets. Example
- Rather than changing the object itself, inject the change on the fly where it's needed, e.g. into the CullVisitor by using a CullCallback. Example
- Set the object's dataVariance to DYNAMIC, so that the draw traversal knows that it has to synchronize that object. Never do this in performance critical areas, or at all, really. Just one DYNAMIC object will make the threading useless and probably halve your frame rate.
The SceneManager is where game object's rendering nodes (.nif or .osg) are loaded, prepared, optimized and instanced, then stored in cache for future use. Instancing refers to making a copy of the object that is safe to use and modify, while the original 'template' object remains read-only.
A state sharing mechanism is employed that will share StateSets across different objects in order to improve rendering efficiency. This means that for any object loaded through the SceneManager it is not safe to modify its StateSets because they could also be in use by another object. It is advised to create a copy of the StateSet before making a modification (just as discussed in Threading considerations). The StateSetUpdater already does this for you by creating a copy of the StateSet on the first frame its used.
The optimizer's job is to restructure a scenegraph in a way that is functionally equivalent yet increases the rendering speed. It does this for example by merging Geometries with the same StateSet into one Geometry, removing redundant nodes, and so on. The Optimizer is run automatically on any object loaded through the SceneManager.
It has been known to happen that the optimizer contained bugs, which are then (usually) promptly fixed. These bugs could cause objects to render incorrectly, cause incorrect collisions or spam warnings on the console. In order to check if a bug is related to the Optimizer, we can run OpenMW with the OPENMW_OPTIMIZE=OFF environment variable. If the bug disappears with that flag, we can further narrow down which specific optimization may be causing it, by disabling them in order:
OPENMW_OPTIMIZE="~MERGE_GEOMETRY" OPENMW_OPTIMIZE="~MERGE_GEOMETRY|~REMOVE_REDUNDANT_NODES" # If the bug does not appear now, the first optimization (FLATTEN_STATIC_TRANSFORMS) has to be the culprit.
Aside from the optimizer, the sceneutil/ directory contains many more OpenSceneGraph "extensions" made for OpenMW purposes, for example our lighting system. It is a good idea to be familiar with these.
There is also a scene graph debugger (invoked in-game with the "showSceneGraph" (ssg) console command), this will print out the scene graph representation of any given object, or even the whole scene (if no object is given). The results are printed to a text file.