4 Aug 2007

The Nebula3 Render Layer: Graphics

The Graphics subsystem is the highest level graphics-related subsystem in the Render Layer. It's basically the next version of the Mangalore graphics subsystem, but now integrated into Nebula, and connected much tighter to the lower level rendering code. The basic idea is to have a completely autonomous graphics "world" with model-, light- and camera-entities and which only requires minimal communication with the outside world. The main operations on the graphics world are adding and removing entities to/from the world, and updating their positions.
Since the clear separation line between Mangalore's Graphics subsystem and Nebula2's Scene subsystem has been removed completely in Nebula3, many concepts can be implemented with less code and communication overhead.

The Graphics subsystem will also be the "multithreading border" for asynchronous rendering. Graphics and all lower-level rendering subsystems will live in their own fat-thread. This is fairly high up in the Nebula3 layer model, but I choose this location because this is where the least amount of communication needs to happen between the game-play related code and the graphics-related code. With some more "built-in autonomy" of the graphics code it should be possible to run the game code at a completely different frame rate then the render code, although it needs to be determined through real-world experience how practical that is. But it's definitely something I'll try out, since often there's no reason to run game play code at more then 10 frames per second (Virtua Fighter fans may disagree).

The most important public classes of the Graphics subsystem are:
  • ModelEntity
  • CameraEntity
  • LightEntity
  • Stage
  • View
A ModelEntity represents a visible graphics object with a position, bounding volume and an embedded Model resource. A Model resource is a complete 3d model with geometry, materials, animations, transformation hierarchies etc... (more on that in a later post).

A CameraEntity describes a view volume in the graphics world. It provides the View and Projection matrices for rendering.

A LightEntity describes a dynamic light source. The exact properties of Nebula3 light sources haven't been laid-out yet, but I'm aiming for a relatively flexible approach (in the end a light source isn't much more then a set of shader parameters).

Stages and Views are new concepts in the Nebula3 Graphics subsystem. In Mangalore there was a single graphics Level class where the graphics entities lived in. There could only be one Level and one active Camera entity at any time. This was fine for the usual case where one world needs to be rendered into the frame buffer. Many game applications require more flexible rendering, it may be necessary to render 3d objects isolated from the rest of the graphics world with their own lighting for use in GUIs, additional views into the graphics world may be required for reflections or things like surveillance monitors, and so on... In Mangalore, the problem was solved with the OffscreenRenderer classes, which are simple to use but were added as an after-thought and have some usage restrictions.

Nebula3 provides a much cleaner solution to the problem through Stages and Views. A Stage is a container for graphics entities and represents its own little graphics world. There may exist multiple stages at the same time but they are completely isolated from the other stages. An entity may only be connected to one stage at a time (although it is easily possible to create a clone of an existing entity). Apart from simply grouping entities into a graphics world, the main job of a Stage is to speed up visibility queries by organizing entities by their spatial relationship. An application may implement radically different visibility query schemes by deriving subclasses of Stage.

A View object renders a view into a stage through a CameraEntity into a RenderTarget. There may be any number of View objects connected to any stage. View objects may depend on each other (also on Views connected to a different stage), so that updating one View may force another View to update its RenderTarget first (this is handy when one View's rendering process requires the content of another View's render target as a texture). View objects completely implement their own render loop. Applications are free to implement their own render strategies in subclasses of View (e.g. one-pass-per-light vs. multiple-lights-per-pass, render-to-cubemap, etc...).

So, in summary, a Stage completely controls the visibility query process, while a View completely controls the rendering process.

One of the main jobs of the Graphics subsystem is to determine what actually needs to be rendered by performing visibility queries between entities. A visibility query establishes bi-directional visibility links between entities. Visibility links come in 2 flavors: camera links and light links. Camera links connect a camera to the models in its view volume. Since visibility links are bi-directional, a camera knows all the models in its view volume, and a model knows all the cameras it is visible through. Light links establish the same relationship between lights and models. A light has links to all models it influences, and a model knows all lights it is influenced by.

The most important class to speed up visibility queries is the internal Cell class. A Cell is a visibility container for graphics entities and child cells. A Cell must adhere to 2 simple rules:
  1. if a Cell is completely visible, all its entities and child Cells must be completely visible
  2. if a Cell is completely invisible, all its entities and child Cells must be completely invisible
Cells are attached to a Stage, and they form hierarchies with one root Cell at the top. The standard Cell class allows simple spatial subdivision schemes like quad-trees or oct-trees, but an application may derive its own Cell subclasses to implement different visibility schemes like portals. The only limitations to the functionality of subclasses are the 2 visibility rules outlined above.

When a graphics entity is attached to a Stage, it will be inserted into the lowest level Cell which "accepts" (completely contains, usually) the entity. When updating the transformation or bounding volume of a graphics entity it will change its position in the Cell hierarchy if necessary.

Stages are populated through the StageBuilder class. An application should derive from StageBuilder to create the initial state of a Stage by adding Cells and Entities to it. Nebula3 comes with a standard set of StageBuilders which should suffice for most applications.

This was just a rough overview of the Graphics subsystem. Since there exists only a very basic implementation at the moment, many details may change over the next few weeks.