-
Notifications
You must be signed in to change notification settings - Fork 165
SceneJS Overview
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.
Rendering each frame straight off the scene graph is a flexible approach, but for large scenes, things like node traversal, rebuilding transform matrices and the accumulation of state for draw calls adds up to a lot of JavaScript to repeatedly execute per frame.
SceneJS attempts to keep the code path that is re-executed per frame as short as possible by compiling the scene graph to an optimised draw list of nodes containing WebGL calls, as shown below. SceneJS optimises the draw list by sorting the nodes into a sequence that is efficient for the GPU to execute, while also avoiding the redundant repetition of WebGL state changes (eg. texture and VBO rebinds).
Whenever the scene graph is subsequently updated by application code, SceneJS is often able to retain the draw list and simply redraw it for the next frame. In most cases, it can just write the updated scene node properties (materials, angles etc) straight through to the draw list nodes, without disrupting them. When app code makes a structural change to the scene graph, SceneJS will recompile only the affected portions of the draw list from the modified scene branches. In short, SceneJS is carefully designed for efficient re-synchronisation of the draw list for scene graph updates.
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 ]
The SceneJS API supports several scene definition techniques that improve scene performance by exploiting the draw list state sorting order described above.
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 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.
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.
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.