Skip to content
Lindsay Kay edited this page Aug 26, 2013 · 22 revisions

What's a Scene Graph?

A scene graph is a data structure that arranges the logical and spatial representation of a graphical scene as a collection of nodes in a graph, typically a tree. Scene graphs can provide a convenient abstraction on top of low-level graphics APIs (such as WebGL) that encapsulates optimisations and API best practices, leaving the developer free to concentrate on scene content.

A key feature of most scene graphs is state inheritance in which child nodes inherit the states set up by parents (e.g. coordinate spaces, appearance attributes etc).

For SceneJS, a benefit of scene graphs is modularity, where JSON subtrees can be complete reusable components.

The snippet below gives an example of a 3D scene created with SceneJS. The scene graph is a directed acyclic graph expressed in JSON, in this case defining a scene containing a blue teapot and two boxes sharing the same textured appearance. Geometry nodes are normally at the leaves, where they inherit the state defined by higher nodes, in this case the material and rotation transform.

var scene = SceneJS.createScene({
    nodes: [{
        type: "material",
        color: {
            r: 0.5,
            g: 0.5,
            b: 1.0
        },

        nodes: [{
            type: "texture",
            layers: [{
                src: "../../textures/superman.jpg"
            }],

            nodes: [{
                type: "translate",
                id: "firstBoxPos,
                x: -3.0,
                y: 1.0,
                z: 5.0,

                nodes: [{
                    type: "prims/box"
                }]
            }, {
                type: "translate",
                y: 1.0,
                z: 5.0,

                nodes: [{
                    type: "prims/box"
                }]
            }]
        }, {
            type: "prims/teapot"
        }]
    }]
});

SceneJS parses that description to create the node graph shown below. Note that geometries at the leaves inherit state from parent nodes, and that various nodes hold resources allocated for them on the GPU.

When aiming at high performance 3D graphics in the browser, the greatest performance bottleneck is JavaScript execution overhead. Therefore, we use scene-to-draw-list compilation to attempt to keep the amount of JavaScript that's executed within each render loop to the minimum.

Traversing the scene graph for each frame is a flexible approach, but tends to be very wasteful due to things like node traversal, traversing redundant nodes, rebuilding transform matrices, and so on.

SceneJS compiles the scene graph to an optimised draw list of WebGL calls, as shown below, in which the calls are sorted so that the state changes they make on WebGL will be closer to the most efficient sequence for rendering the frame. In many cases it's able to retain the draw list and simply redraw it for each frame, while copying property updates on scene nodes (eg. material color, rotation angle etc.) straight into the corresponding draw list nodes. When a structural change is made to the scene graph, SceneJS will just recompile only the affected portions of the draw list from the modified scene nodes or branches.

This is much faster than rendering straight off a scene graph traversal for each frame. Furthermore, if we want to do state sorting (a staple technique in 3D rendering), we would need to rebuild and resort a new draw list after each traversal.

Shader Generation

Lost WebGL Context Recovery

The GPU is a shared resource, and there are times when it might be taken away from our WebGL applications, such as when there are too many applications holding them, or when another application does something that ties up the GPU too long (perhaps even a DOS attack via shader, perish the thought). In such cases the operating system or browser may decide to reset the GPU to regain control. When this happens, our WebGL apps will need to reallocate their textures, VBOs, shaders etc. on a new context afterwards. SceneJS takes care of that recovery transparently, without disruption to code at higher layers. When WebGL restores the context again, SceneJS automatically reallocates the shaders and buffers from data it retains in its scene graph, without needing to reload anything off the server, and without loss of any scene state. It's surprising how quick that recovery is - SceneJS was benchmarked at around 5-7 seconds to recover a scene containing approximately 2000 meshes and 100 textures. [ Demo ]

Scene Optimisation

The SceneJS API supports several scene definition techniques that improve scene performance by exploiting the draw list state sorting order described above.

Texture Atlases

A texture atlas is a large image that contains many sub-images, each of which is used as a texture for a different geometry, or different parts of the same geometry. The sub-textures are applied by mapping the geometries’ texture coordinates to different regions of the atlas. SceneJS sorts its draw list by shader, then by texture. So long as each of the geometry nodes inherit the same configuration of parent node states, and can therefore share the same shader, the draw list will bind the texture once for all the geometries. Another important benefit of texture atlases is that they reduce the number of HTTP requests for texture images.

VBO Sharing

VBO sharing is a technique in which a parent geometry node defines vertices (consisting of position, normal and UV arrays) that are inherited by child geometry nodes, which supply their own index arrays pointing into different portions of the vertices. The parent VBOs are then bound once across the draw calls for all the children. Each child is a separate object, which as shown in Figure SceneJS:vboSharingListing, each child geometry can be wrapped by different texture or materials etc.

Shareable Node Cores

Traditionally, re-use within a scene graph is done by attaching nodes to multiple parents. For dynamically updated scenes this can have a performance impact when the engine must traverse multiple parent paths in the scene graph, so SceneJS takes an alternative approach with ”node cores”, a concept borrowed from OpenSG. A node core is the node’s state. Having multiple nodes share a core means that they share the same state. This can have two performance benefits: An update to shared node can write through to multiple draw and call list nodes simultaneously. There is increased chance of identical repeated states having matching IDs when executing the call list, which as described in Section 0.2.2, tracks the state IDs to avoid redundantly reapplying them. Listing 5 shows an example of node core sharing through the scene definition API.

Custom Scene Node Types

New node types can be provided as plugins. This allows us to define our own high-level (perhaps even domain-specific) scene components that just slot straight into the scene graph as nodes which you create and update as usual via the JSON API. Custom nodes are effectively actors, which may create and manage subtrees of child nodes beneath themselves, including other custom node types. SceneJS comes with a growing library of various custom node types, such as cameras, geometric primitives, special effects, buildings, vehicles, and so on.

Clone this wiki locally