30.3.  Optimizing Rendering

The main goal of performance tuning is to make the application look and feel faster. However, just because the goal is to make the application render faster, don’t assume that rendering is the bottleneck.

To find out whether rendering is the problem, modify your application so that it does everything it normally does except render, and then measure its performance. An easy way of getting your application to do everything but rendering is to insert an SoSwitch( C++ | Java | .NET )node with its whichChild field set to SO_SWITCH_NONE (the default) above your scene. So, for example, modify your application’s code from:


C++
myViewer->setSceneGraph(root);
  

.NET
myViewer.SetSceneGraph(root);
  

Java
myViewer.setSceneGraph(root);
  

to:


C++
SoSwitch *renderOff = new SoSwitch;
renderOff->ref();
renderOff->addChild(root);
myViewer->setSceneGraph(renderOff);
  

.NET
SoSwitch renderOff = new SoSwitch();
renderOff.AddChild(root);
myViewer.SetSceneGraph(renderOff);
  

Java
SoSwitch renderOff = new SoSwitch();
renderOff.addChild(root);
myViewer.setSceneGraph(renderOff);
  

This experiment gives an upper limit on how much you can improve your application’s performance by increasing rendering performance. If your application doesn’t run much faster after this change, then rendering is not your bottleneck. See Section 30.4, “ Optimizing Everything Else” for information on optimizing the rest of your application.

If you have determined that your application is spending a significant amount of time rendering the scene, the next step is to isolate rendering from the rest of the things your application does. This makes it easier to find out where the bottleneck in rendering occurs. The easiest way to isolate rendering is to write your scene to a file and then use the ivperf program to perform a series of rendering experiments. The code for writing your scene may look like the following:


C++
SoOutput out;
if (!out.openFile("myScene.iv")) { ... error ... };
SoWriteAction wa(&out);
wa.apply(root);
  

.NET
SoOutput @out = new SoOutput();
if (!@out.OpenFile("myScene.iv")) { ... error ... };
SoWriteAction wa = new SoWriteAction(@out);
wa.Apply(root);

Java
SoOutput out = new SoOutput();
if (!out.openFile("myScene.iv")) { ... error ... };
SoWriteAction wa = new SoWriteAction(out);
wa.apply(root);

The ivperf utility reads in a scene graph and analyzes its rendering performance. It estimates the time spent in each stage of the rendering process while rendering the scene graph.

The process of rendering a single frame can be decomposed into five main stages:

The sum of the times spent in these stages does not, in general, equal the total time it takes to render the scene. Depending on the underlying hardware platform and graphics pipeline, some or all of the above can overlap with each other. Thus, completely eliminating one of the stages does not necessarily speed up the application by the time taken by that stage. ivperf takes this into account; it answers questions of the type “if I could completely eliminate xxx from my scene, how much faster would rendering be?” For example, if ivperf indicates that 50% of your time is spent changing the material graphics state, then making your entire scene a single material would make it render twice as fast. Knowing that materials are taking up a significant part of your rendering time, you can then concentrate on minimizing the number of material changes made by your scene.

If you have created your own node classes, call their initClass() methods just after the call to SoInteraction::init()in the ivperf source and link their .o files into ivperf.

The camera control used by ivperf is simplistic: it calls viewAll() for the scene and just spins the scene around in front of the camera when benchmarking. If you have a sophisticated walk-through or fly-through application that uses level of detail and/or render culling, modify ivperf so that its camera motion is more appropriate for your application. For example, have ivperf use the following little scene instead of just SoPerspectiveCamera( C++ | Java | .NET ) :

TransformSeparator
  {
  Rotor { rotation 0 1 0 .1 speed .1 }
  Translation { translation 100 0 0 }
  PerspectiveCamera { nearDistance .1 farDistance 600 }
  }


ivperf correctly reports the performance of changing scenes, as long as you give it enough information. It automatically deals with scenes containing engines and animation nodes, but if you are using an SoSensor( C++ | Java | .NET )to modify the scene, you should mark nodes that your application frequently changes by giving them the special name “NoCache”. For example, if your application is frequently changing a transformation in the scene, the transformation should appear in the file given to ivperf as:

DEF NoCache Transform { }

The first step in the rendering process is clearing the window. It is easy to forget this step, but depending on the size of your application’s window and the type of system you are running on, clearing the window can take a surprisingly long time. If your application’s main window is typically 1000 by 1000 pixels, run ivperf like this:

ivperf -w 1000,1000 myScene.iv

ivperf performs many different rendering experiments, and eventually prints information on each rendering stage.

For example, on an Indigo2™ Extreme™ running IRIX 5.3, ivperf reports that for rendering a simple cube in a 1000 by 1000 pixel window 46% of the time is spent clearing the window.

Unfortunately, if clearing the window takes too much time, there is not a lot you can do. One possibility is to make the window’s default size smaller (while still allowing users to resize the window if necessary).

After running ivperf, you know how much time your application spends clearing the color and depth buffers. The next experiment is designed to find out how much time Open Inventor spends traversing your scene. Traversal is the process of walking through the scene graph, deciding which render method needs to be called at each node. Open Inventor automatically caches the parts of your scene that aren’t changing and that are expensive to traverse, building an OpenGL display list and eliminating the traversal overhead.

If most of your scene is changing, or if your scene is not organized for efficient caching, Open Inventor may not be able to build render caches, and traversal might be the bottleneck in your application. ivperf measures the difference between rendering your scene with nothing changing, and rendering with the camera, engines, and nodes named “NoCache” changing.

If traversing the scene is a bottleneck in your program, there are several ways of reducing the traversal overhead:

You may be able to organize your scene so that Open Inventor can build and use render caches even if part of the scene is changing. Note that the following things inhibit caching:

If ivperf reports that material changes are the rendering bottleneck, try the following:

This works only for PER_PART_INDEXED or PER_FACE_INDEXED material bindings.

For transformation, ivperf reports two numbers: the overhead of changing the OpenGL transformation matrix between rendering shapes and the time it takes to transform the vertices in your scene through that matrix. This section helps with the former, giving suggestions on how to make Open Inventor execute fewer OpenGL matrix operations. See the section called “Optimizing Vertex Transformations” for hints on optimizing the transformation of vertices.

For best performance when creating SoFaceSet( C++ | Java | .NET )and SoIndexedFaceSet( C++ | Java | .NET )shapes, arrange all the triangles first, then quads, and then other faces.

If your scene contains textures, ivperf reports two numbers: the time you would save if you could turn off textures completely, and the time you would save if you could make your scene use only one texture. On systems with texturing hardware, the number of textures used can dramatically affect performance; see the section called “ Optimizing Texture Management” for hints on optimizing texture management. On systems without texture mapping hardware, the bottleneck is probably filling in the textured polygons.

Open Inventor automatically does two things to speed up rendering on systems without texture mapping hardware:

  • Open Inventor’s viewers display the scene untextured during interaction by default.

  • Open Inventor uses lower-quality filters for minifying or magnifying textures.

If ivperf reports a lot of time is spent in texture management, then you are running out of hardware texture memory. Try the following:

If the scene given to ivperf contains light sources, ivperf informs you how expensive they are compared to rendering your scene with just a single directional light. If ivperf reports that lights are a significant performance bottleneck, try to use fewer light sources, and use simpler lights (a DirectionalLight is simpler than a PointLight, which is simpler than a SpotLight). If possible, put lights inside separators so that they affect only part of the scene, increasing performance for the rest of the scene.

If ivperf reports that vertex transformations (which include per-vertex lighting calculations) take up a significant portion of the time it takes to render a frame, you can do the following to optimize per-vertex operations:

See the section called “ Making Open Inventor produce efficient OpenGL” for hints on making Open Inventor produces more efficient OpenGL calls.

A common bottleneck on low-end systems is drawing the pixels in filled polygons. This is especially common for applications that have just a few large polygons, as opposed to applications that have lots of little polygons.

If ivperf reports that a large percentage of each frame is spent filling in pixels, try to optimize your scene as follows:

There are several performance problems that ivperf doesn’t catch. The following sections describe them, and give hints on how to improve them.

If your application is rendering only 10 frames per second with 1,000 triangles per frame, and you know that your graphics hardware is capable of rendering 100,000 triangles per second (10,000 triangles per frame at 10 frames/second), and ivperf reports that your bottleneck is vertex transformations, then your problem might be that Open Inventor is not making efficient OpenGL calls.

Open Inventor is much more efficient at rendering multiple triangles if they are all part of one node. For example, you can create a multifaceted polygonal shape using a number of different coordinate and face set nodes. However, a much better technique is to put all the coordinates for the polygonal shape into one SoCoordinateor SoVertexProperty( C++ | Java | .NET )node, and the description of all the face sets into a second SoFaceSet( C++ | Java | .NET )node.

[Tip]

The ivfix utility program collapses multiple shapes into single triangle strip sets. Using fewer nodes to get the same picture reduces traversal overhead for scenes that cannot be cached. Note also that Open Inventor optimizes on a node by node basis and generally can’t optimize across nodes.

An SoFaceSet( C++ | Java | .NET )or SoIndexedFaceSet( C++ | Java | .NET )has special code for drawing 3- and 4-vertex polygons. To take advantage of that, you must arrange the polygons so that the 3-vertex polygons (if any) are first in the coordIndexarray, followed by the 4-vertex polygons, followed by the polygons with more than 4 vertices.

For some applications, consider implementing your own nodes that implement the functionality of a subgraph of your scene. For example, a molecular modeling application might implement a BallAndSticknode with fields specifying the atoms and bonds in a molecule, instead of using the more general SoSphere( C++ | Java | .NET ) ,SoCylinder( C++ | Java | .NET ) ,SoMaterial( C++ | Java | .NET ) ,SoTransform( C++ | Java | .NET ) , and SoGroup( C++ | Java | .NET )nodes. If the molecular modeling application changes the molecule frequently so Open Inventor cannot cache the scene, using a specialized node could make traversal orders of magnitude faster (for example, a simple water molecule scene graph with three atoms and two bonds might consist of 20 nodes; replacing this with a single BallAndStick node would make traversal 20 times faster). The BallAndStick node could also perform application-specific optimizations not done by Open Inventor, such as not drawing bonds between spheres whose radii were large enough that they intersected, sorting the spheres and cylinders by color, and so on. See The Open Inventor Toolmaker for complete information on implementing your own nodes.

If your application uses SoLOD( C++ | Java | .NET )nodes, it might be spending a significant amount of time deciding which level of detail should be drawn. One way of testing to see if this is the case is to temporarily replace all of the SoLOD( C++ | Java | .NET ) nodes in your scene with SoSwitch( C++ | Java | .NET )nodes set to traverse the highest level of detail. Then run ivperf again and compare the results. If the SoSwitch( C++ | Java | .NET ) node scene is much faster, try doing the following:

  • Try to group objects so that one level of detail test determines the level of detail for several objects. For example, if you have a group of 10 buildings that are near each other, use one level of detail node instead of 10 level of detail nodes. Doing this also makes it easier for Open Inventor to build larger render caches, which may increase performance by increasing traversal speed.

  • Make sure you use the SoLOD( C++ | Java | .NET ) node introduced in Open Inventor 2.1 instead of the SoLevelOfDetail( C++ | Java | .NET )node. The SoLOD( C++ | Java | .NET ) node is more efficient because it uses the distance to a point as the switching criterion. See the reference page for more detail.

When manipulating a complex scene, it is often useful to temporarily change or decrease the value of some parameters in order to maintain interactive performance. The SoInteractiveComplexity( C++ | Java | .NET ) node allows the application to define different parameter values for certain fields of other nodes, depending on whether a user interaction, for example moving the camera, is occurring. This means that while the camera is moving these fields will use a specified "interaction" parameter value, but when interactive manipulation is stopped these fields will automatically change to a specified "still" parameter value. Optionally, for scalar fields, the transition from interaction value to still value can be automatically animated using a specified increment. This is a powerful technique for maintaining an interactive frame rate when interacting with GPU intensive datasets or rendering effects, while still getting a final image with very high quality and also giving the user a "progressive refinement" effect while transitioning from interaction back to "still".

The values specified in SoInteractiveComplexity( C++ | Java | .NET ) override the values in the fields during rendering, but calling getValue() on the fields still returns the value set directly into the field (or the default value if none was set). These settings are applied to all instances of the node containing the field and are declared with a specially formatted string set in the fieldSettings field. For scalar fields like SoSFInt32( C++ | Java | .NET ), the string looks like this:

"ClassName FieldName InteractionValue StillValue [IncrementPerSecond]"

If IncrementPerSecond is omitted, then StillValue is applied as soon as interaction stops. Else the transition from InteractionValue to StillValue is automatically animated. Because incrementing is actually done at each redraw, and redraw happens many times per second, IncrementPerSecond is allowed to be greater than StillValue. In the following code, the field named numSlices belonging to the class SoVolumeRender will be set to 500 during an interaction. When the interaction stops, numSlices will be increased by 2000 every second until its value reachs 1000. Effectively this means that the StillValue (1000) will be reached in (1000-500)/2000 = 0.25 seconds.

SoInteractiveComplexity* interactiveComplexity = new SoInteractiveComplexity;
interactiveComplexity->fieldSettings.set1Value(
0, "SoVolumeRender numSlices 500 1000 2000" );
root->addChild( interactiveComplexity );
root->addChild( volumeRender );

A time delay before changing the value, or starting the animation, can be set using the refinementDelay field.

Note that only a limited number of fields are supported by this node. See the reference manual for the current list.

SoComplexity::value
SoComplexity::textureQuality
SoShadowGroup::quality
SoShadowGroup::isActive,
SoVolumeRender::numSlices
SoVolumeRender::lowScreenResolutionScale
SoVolumeRenderingQuality::gradientQuality
SoVolumeRenderingQuality::lighting
SoVolumeRenderingQuality::edgeDetect2D
SoVolumeRenderingQuality::boundaryOpacity
SoVolumeRenderingQuality::edgeColoring
SoVolumeSkin::largeSliceSupport
SoOrthoSlice::largeSliceSupport