Skip to content

Technical Layout of Ardor3D

Joshua Slack edited this page Sep 19, 2017 · 1 revision

Scene graph in Ardor3D

Scene graphs allow you to describe a 3d scene or view as a tree of nodes. Arranging a scene this way enables many optimizations for the purposes of rendering, such as being able to skip drawing entire portions of the tree based on checking the visibility of a given node in the tree or applying a visual look to the entire scene by applying it to the base of the tree.

When an object is part of a scene graph, it is possible to look at it in more than one context. For example, if you took any member (called “Spatial” in Ardor3D) of the tree you can still consider it a structure unto its self, positioned locally in space (transformed) a certain way, with its own given coloring, lighting, or other such properties (render states). When we talk about such “inherent” characteristics in Ardor3D, we refer to them as “local” characteristics or fields.

On the other hand, we can also consider these same characteristics with regards to where the Spatial is in the scene graph tree. When you arrange one Spatial to be the child of another Spatial, the child is influenced by its parent (and grandparent, etc.) For example, if I have a Spatial that is rotated 90 degrees and another that is rotated 45 degrees and I attached one as the child of the other, the child will be rotated a total of 135 degrees. Other physical properties are inherited similarly, often using some type of hint to indicate how properties should inherit or override each other. These altered versions of the local properties are stored separately and are referred to in Ardor3D as “world” fields.

Scene graph class structure

Ardor3D has a scene graph class structure very similar to jME's. As mentioned above, at the base is the Spatial class which has a world bound, local and world transform information, local render states, a parent reference, controllers and possibly a user data object. Spatial is an abstract class and therefore you would work directly with a class that extends it such as Node or Mesh.

The Node class extends Spatial with a List of Spatialchildren and logic regarding dealing with children. Node is the glue that binds together your scene graph since only a Node can be the parent of a Spatial object.

The Mesh class also extends Spatial and is your general purpose scene graph tree “leaf”. It adds a local “model” bound, mesh data buffers (in a MeshData field) and other odds and ends such as default color and a list of compiled (“world”) states.

The MeshData class is a container for the various data buffers that can be used to construct scene geometry (currently: vertices, indices, normals, texture coordinates, tangents and vertex colors.) It can store this data as individual Java NIO buffers, or interleaved as a single data buffer (with a separate index buffer).

Typically MeshData is used to describe a single contiguous type of geometry such as a set of triangles, a single triangle strip, a collection of lines, etc. but it also has two fields (indexLengths and indexModes) that can be used to indicate that the indices should be split into multiple geometries, each potentially composed of a different type of primitive.

Spatial - Notable Methods

draw(Renderer)

Current draw process:

1. Node.draw-> child.onDraw
2. camera is checked on child  
       *  If not in frustum, end.  
       *  Else if is Node, goto 1.  
3. Mesh child → add to bucket  

When we render our buckets:

1. Renderer.render queue → sorts the buckets, draws them, then clears them
2. drawing a queue calls draw, but since we are in bucket mode, Mesh.draw now calls Renderer.render(Renderable)
3. Renderer.render(Renderable) → Mesh.render(Renderer)
4. Mesh.render calls various Renderer functions (transform, setup buffers, drawElements, etc.) 

(COME BACK TO THIS AFTER FORUM DISCUSSION)

Spatial.updateGeometricState(double timeInSeconds [, boolean initiator=true]):

 1. Updates local controllers
 2. If we have a dirty world Transform (we've altered it since last updateGeometricState call), update our world Transform.
 3. If we have one or more dirty world RenderStates, update our world RenderStates.
 4. Call to an extendable method “updateChildren(double time)” to allow updating of scene components under this Spatial. For example, if this Spatial is a Node, updateGeometricState would now be called on all children of the Node. This allows for child changes that could affect our world properties (a controller affecting a child's bounding volume for example) to happen before we update our world bound.
 5. If our world bound is dirty, update it. If we were the “initiator”, tell our parents to update their world bounds. 

Important Application Interfaces / Life cycle

  • Canvas (interface): Is our tie into a Windowing System/Toolkit. It sets up the container for our GL surface and owns a CanvasRenderer. (implemented by: AwtCanvas, SwtCanvas, NativeCanvas)

  • CanvasRenderer (interface): Sets up and is responsible for a GL surface using a specific GL binding. Owns a default Camera and a Scene, as well as a Renderer implementation. (implemented by: LwjglCanvasRenderer, JoglCanvasRenderer)

  • Scene (interface): Describes something to render, whether that is a single Node or a complex system of multiple passes and components. Is called when it is time to render the described scene. Also handles picking requests.

  • Updater (interface): Describes some type of actions to take once per frame. Breaks update logic out from the view (Scene).

  • FrameHandler (class): Maintains a timer and registered lists of Canvas and Updater objects (not necessarily the same size). On calling of the method FrameHandler.updateFrame() the following actions are taken:

    1. Calculates the time since the last frame (double precision, in seconds) using our nanosecond Timer class.
    2. Traverses the list of Updater objects, calling update(time) on each.
    3. Constructs a CountDownLatch the size of the current Canvas list. This is useful for allowing the drawing of Canvases to potentially take place on another thread while we wait for everyone to finish.
    4. Traverses the list of Canvas objects, calling draw(latch) on each. In the Canvas, when the draw has completed, we'll decrement the latch.
    5. Wait for the latch to reach 0. If more than 5 seconds pass, log a warning.

Important Input System classes

  • PhysicalLayer: responsible for tracking keyboard state, mouse state and focus for a canvas using implementation specific wrapper classes. The PhysicalLayer does not necessarily poll the hardware directly – rather, it has references to wrapper classes that are responsible for making sure recent input events are available when requested. Two important public methods:

readState() → simply asks mouse and keyboard wrappers for recent events, turning them into “input states” Loops, and on each loop:

1. iterates through any next available event for mouse and keyboard, updating an internal representation of the current state of both (ie. What keys are down, what buttons are pressed, etc.)
2. on each loop, adds a copy of the current mouse and keyboard state to a stateQueue.
3. Breaks from loop if no further mouse and/or key events. 

drainAvailableStates() → empties the current stateQueue into a linked list for processing by caller.

  • LogicalLayer: responsible for matching Triggers to input state changes. It is possible for 1 LogicalLayer to listen to multiple PhysicalLayers (but not the other way around.)

checkTriggers(double time) → this is the method you would use to “update input” It handles both updating the PhysicalLayers assigned to it, as well as executing relevant Triggers. This is done as follows:

1. For each of our registered PhysicalLayers (each tied to a specific Canvas source):
    1. Call readState and drainAvailableStates to grab new state changes.
    2. If no state changes, apply any Triggers that should execute when state remains the same. (sounds odd, but basically this allows for things like “repeat action when holding button” etc.)
    3. Otherwise, for each state change we received, apply any Triggers that should execute when the state moves from our last state, to this new state. (then update last state to this one.) 

Triggers

The Ardor3D input system works off of triggers, basically a simple “if this happens, do this”. For this, we use the Google common objects API – Predicate interface. The Predicate interface has a method called apply that is passed an Object and returns a boolean. The same API also has a class called Predicates that has several useful methods for creating new Predicate objects that describe various logical combinations of existing Predicate objects. For use with Predicate, we have a data object called TwoInputStates, which is simply a set of two, non-null InputState objects. Each InputState object describes the state, as Ardor3D sees it, of the keyboard and mouse at a moment in time.

To use all of this in our Trigger system, we create new Predicate objects, parameterized with the TwoInputStates class. In the apply method, we add logic to test changes between the two InputStates. So for example, if we wanted a Predicate that returns true when the spacebar is pressed, we could check the list of keys down in the second InputState and if spacebar is down, but is not listed as down in the first InputState, we would return true. (There's a helper method called getKeysPressed that assists with that.) InputTrigger and TriggerAction are how we tie these Predicate objects to an actual action. InputTrigger is simply a key value pair of Predicate to TriggerAction. The TriggerAction interface defines a perform method. InputTriggers are registered with LogicalLayer. During update, the Predicate is tested, and if found true, the corresponding action is executed. The perform method takes as parameters the source Canvas (from the PhysicalLayer that generated the state change) as well as the current InputState and the time per frame from our nanosecond Timer.

Math

Some notes about our Math classes:

  • Our math classes are internally stored in double precision. The math library exists as a replaceable jar which would allows you to switch out for other internal representations.
  • Classes are designed to allow for change tracking by never exposing internal fields.
  • All classes implement Cloneable, Savable and Externalizable.
  • Each math class also implements a ReadOnly* interface which outlines methods that are safe to execute (guaranteed not to mutate the value of the class.)
  • When non ReadOnly versions are used in our Spatial classes (to represent Transforms), we only allow indirect access so we can track any changes and automatically handle updates appropriately.
  • Each math class also provides methods for fetching and releasing pooled instances of that class. This is useful for reducing object creation, particularly when interacting with classes where the mutable version of a class field is not accessible. (Pooling can be disabled via setting the system property, ardor3d.noMathPools.)

Ardor3D and OpenGL Context

Ardor3D tracks the OpenGL state machine via the RenderContext object. This class maintains changes we make to GL such as render states, the current camera, and so forth. Generally RenderContext is tied to a specific OpenGL context, but it is possible to share GL contexts (basically, to share textures, display lists, etc. but not the applied render states, camera…) Sharing is done by providing the RenderContext to share with during construction. At this time, only Jogl canvas renderers are definitively known to work with sharing. Also important to context is the ContextManager, a class that maintains a set of known RenderContext's as well as a pointer to the RenderContext considered “current”.