Rendering Architecture

From OpenMW Wiki
Jump to navigation Jump to search

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.

Threading considerations

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.

Important components

SceneManager

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.

State Sharing

In addition to the Optimizer, the scene manager employs the osgDB::SharedStateManager to improve rendering efficiency in two ways - sharing StateAttributes and StateSets. Sharing these greatly improves the rendering speed in osg. osg's draw stage compares the active StateAttributes by pointer so if a pointer remains the same during a consecutive rendering of similar objects, we can avoid the associated OpenGL state changes. Shared StateSets perform a similar duty on a more coarse grained level. "State sorting" during the cull stage builds a coarse grained graph of state grouped by the StateSet pointers, allowing draws to be submitted in a certain order that minimizes state changes.

Because of this mechanism, it is not safe to modify the StateSets of objects loaded through the SceneManager - they could be shared with another object. To make modifications, use one of the approaches described in Threading considerations. You can also attach a Node on top of the original Node with a second StateSet containing only the changes you want to make with an OVERRIDE flag.

Be aware that cloning a StateSet has performance implications beyond the clone itself - the clone will negate the substantial benefit of the state sharing mechanism employed earlier. If at all possible, do not use this approach for objects used many times in the scene. There are plenty of alternative approaches as described earlier.


Optimizer

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.

Utilities

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.