Local Info

Topics

j3d.org

Rendering Pipeline Notes

The rendering pipeline of Aviatrix3D is a relatively simple structure. In fact, if you picked up a 3D graphics architecture book and compared the idealised version to our code, you'll see a very strong correspondence in the designs. At the lowest level, the pipeline setup is a simple design with extensibility being the primary design goal. Because 3D content can vary so dramatically, various parts of a pipeline may cause unneeded overhead or have a huge performance increase. We choose the extensibility route because it allows the end user to select the culling and sorting algorithms that are most appropriate to their content's needs.

 

Classes and Interfaces

Because code may find itself in any number of different rendering situations, all of the rendering pipeline is expressed as a collection of interfaces. Individual expressions of parts of the pipeline are then implemented as classes that implement these interfaces. In this way pieces can be mixed and matched in a way that minimises the need to re-implement the same code as hardware and system requirements change. For example, on single processor machines, it's typically not worthwhile having the rendering pipeline be multithreaded, so the pipeline is implemented as a single thread. However, moving the code to run on a multiprocessor machine means just replacing the pipeline management implementation without needing to replace the culling and sorting implemenation, nor the output device handling.

Parts of the Pipeline

In a classic 3D graphics pipeline, there are 3 basics parts, typically refered to as: APP, CULL and DRAW stages. Our design uses this basic principle and divides the CULL stage in two separate sub stages - SORT and CULL. The SORT stage sits after CULL and before DRAW, and takes care of any sorting that may need to be done after the culling has taken place. Typical examples are state sorting and depth sorting for transparency rendering.

The APP stage is represented by the RenderManager interface. Here all of the synchronisation and basic rendering control is implemented. It's job is to marshall all the bounds and data update requests, process them, synchronise the rest of the pipeline and rendering, as well as interact with the user-level code.

Once the user-level code has been processed, the CULL stage is represented by the RenderPipeline interface. This generic interface is responsible for managing the two sub-parts: the CULL and SORT stages. These stages are represented, naturally enough, by the CullStage and SortStage interfaces. The pipeline management may, itself be either single or multi threaded depending on the user's requirements. Either way, it is responsible for making sure that data gets from one stage to the next efficiently and safely.

At the end of the rendering process is some sort of surface to draw onto. The implementation of the surface is represented by the DrawableSurface interface, which defines a number of methods for interacting with the pipeline and user code (eg for picking).

RenderPipelineManager

The RenderPipelineManager interface handles all of the synchronisation and timing between the user code and the rest of the rendering process. The implementation of this interface can vary depending on the required usage. As part of the standard classes we've provided implementations that are single threaded and multithreaded. The single-threaded instance is typical of the gamer-style rendering architecture as it sequentially calls user code, processes the changes, fires the rendering pipeline and finally tells the surface to draw the results. The multi-threaded version creates separate threads for each pipeline and keeps them all in check.

Available Methods

The idea behind the design of the interface methods is to allow as much freedom as possible, with API calls to provide the essential information.

Firstly there are methods to add and remove pipeline implementations. Depending on the type of manager, this may be a set/delete operation rather than add/remove. The manager should not assume a particular implementation of the RenderPipeline interface.

Controlling the rendering is the other aspect of this interface. Three methods are provided for this task: setEnabled(), setMinimumFrameInterval() and renderOnce(). Between them, either individual rendering passes can be used or automated handling and timing can be set. Enabling and disabling the render manager controls the entire rendering process. When disabled, nothing happens at all, except for the ability to register node update listeners (which won't be called until the next time a render is performed).

Internal supporting interfaces

Internal to the implementation of the RenderPipelineManager are other important interfaces. These are used to communicate between the manager implementation and node implementations. These interfaces are used as an abstraction so that implementations can be separated from the basic functionality without requiring either cyclic compile dependencies or direct knowledge of a particular node structure and scene graph relationships.

The NodeUpdateHandler interface is the most important of these internal interfaces. This represents the code that can be queried for various runtime state information as well as register state. For example, when user code interacts with a TransformGroup and would like to write the transformation matrix, the TransformGroup queries the instance of this interface it has been given to see if it should permit the user to perform the action.

The PickingHandler interface is used to abstract the internal picking implementation. Two generic picking methods are provided that can be called to evaluate the pick request. Implementations of this interface should be able to handle at least a partial subset of the available picking request (where a request is wrapped in an instance of the PickRequest class).

The InternalNodeUpdateListener is the final part of the system. After all the update callbacks have been performed by the render manager implementation, we need to tell the various nodes to update their bounds. That's the purpose of this interface - to have the rendering manager tell the implementing node when it's suitable to start updating it's bounds. While there is only one method here currently, it is expected others will be added over time.

RenderPipeline

Encapsulating all of the internal processing to go from a scene graph to the OpenGL commands is the job of an implementation of the RenderPipeline interface. As input, the root of the scene graph to be rendered is taken, and as output, a collection of drawing commands are issued.

In reality, the implementation of this interface is quite trivial - most of the hard work is performed by the two working interface stages - CullStage and SortStage. The job of the implementation of this interface is to manage the data transport and synchronisation between the two internal stages. As such, it is expected that it would be very rare that a custom implementation of this class beyond the defaults provided with Aviatrix3D will be used.

CullStage

Handling the basics of culling unneeded parts of the scene graph is the job of the CullStage implementation. This class implements the first part of the geometry pipeline that performs culling operations on the scene graph to render the parts you need. Most typically, this involves view frustum culling, but it may also involve other operations that have to depend on the scene graph traversal.

Apart from the basic culling, the main requirement of this class implementation is to take the heirarchical structure of the scene graph and push it down into a collection of flat structures that basically consists of a single transformation matrix, the Shape3D node to be rendered and any associated rendering data, such lights. This flattened form of the scene graph (collected in an instance of CulledOutputData) will then be ready for sorting and rendering by later stages.

Since this stage is traversing and flattening the scene graph, it is also possible to include other inherent behavioural capabilities. For example, if you wanted to implement a LOD node that should automatically change the selected geometry based on the distance of the viewpoint from the geometry, this would be the place to do it. While you could do it outside in the normal application space, the disadvantage is that if it is under a shared graph then only a single level would be viewable by all paths. Implementing the behaviour here would allow the selection of different levels of detail depending on which path from the root of the scene graph was being evaluated. Such behaviour is not included as part of the default toolkit. It may be offered as part of the utility code at a later date.

SortStage

After the scene graph has been reduced to a flat structure, then sorting needs to take place. The job of sorting it to improve performance and/or visual correctness as well as organise the data into a form that can be processed by the drawing surface(s).

During the sort stage, it may require disassembly of the incoming culled data into smaller chunks. The most popular form of this is state sorting - reducing the number of times you turn on and off state while rendering the geometry. There are various different ways of doing this, but the most popular forms of sorting have already been taken care of at the node level by implementing the Comparable interface and the equals() and hashCode() methods.

The output of the sorting is a list of rendering commands and references to objects to be rendered. The rendering commands available are defined in the RenderOps class. Note that the output should be symmetrical - for every start command, there should be a corresponding Stop command at some point.

DrawableSurface

The last part of the pipeline is the surface. This takes the sorted output and then (almost) blindly applies it at the right time to the GL context. The surface implementation needs to carefully deal with combining all the elements of the rendering process together. Backgrounds, viewpoints, fog and the various rendering operations must be ordered in the correct order. Because the node implementations only care about their specific rendering instructions the surface will still need to do a lot of work in preparation for each of these.