Org.simantics.scenegraph
org.simantics.scenegraph (SVN) is a scene graph library for 2D rendering with a focus on supporting remote rendering using a client-server architecture.
Contents
Overview
org.simantics.scenegraph provides a generic serializable scene graph datastructure ( wikipedia:Scene graph ) for displaying 2D and 3D graphics.
The idea of scene graph is that it is an independent datastructure that can be rendered as such. It can only access local variables, thus it provides a strict interface between the visualization and program logic. Mainly for this reason scene graph is an ideal datastructure for rendering graphics remotely.
Architecture
org.simantics.scenegraph is designed to be used for rendering graphics locally on desktop application, but also remotely from server to client. For this reason, it has some features that a normal scene graph implementation doesn't have.
In org.simantics.scenegraph, the scene graph datastructure is a tree consisting of ParentNodes and Nodes.
- Node is a leaf in the tree, thus it cannot have children, but it should be used to render visible objects.
- ParentNode is used to group a set of nodes (children) and to give some common parameters for the children.
To enable scene graph datastructure synchronization from a server to a client, a mechanism for detecting changes in the datastructure is required. For this purpose, all changes to Node variables must be made through a public method (usually setter). These methods that change local variables are marked with annotations, thus the solution is very similar to what Hibernate uses with entities.
Node methods can have four different annotations, three for using scene graph remotely, and one for synchronizing node variables automatically from the graph:
- SyncField(..) When a method annotated with this is called, the fields listed in the annotation are updated to the client side (if running in remote mode)
- ServerSide Methods with ServerSide annotation are intended to be executed on server-side, thus it can access resources outside the scenegraph datastructure (using interfaces)
- ClientSide Methods with this annotation are executed on client-side, thus it should only access resources inside the scenegraph datastructure
- PropertySetter This annotation is used to map graph properties to node automatically. This annotation can only be used in setter methods, that take one argument. Notice that the argument type must match with the object that is read from the graph (i.e., for double values you must use Double type instead of double, float, or Float).
Without these annotations, the Node will not work properly when using scenegraph remotely.
These annotations (except PropertySetter) are handled with cglib MethodInterceptor which is a proxyclass for the actual class implementation, and contains an interceptor for performing actions before and after the actual method is called. For this reason, the Node classes should not be instantiated directly, but through ParentNode.addNode
method. Cglib support is implemented in org.simantics.scenegraph.remote plugin, and thus org.simantics.scenegraph plugin does not depend on cglib.
Scene graph sub-nodes must not be instantiated directly, but through ParentNode.addNode
orParentNode.getOrCreate
methods. Only the root node may be instantiated directly.
ParentNode and Node classes implement only functionalities for creating and modifying the scene graph datastructure but do not contain any methods for rendering.
2D
Java 2D API compatible implementation of the scene graph is in org.simantics.scenegraph.g2d package:
- G2DNode is a baseclass for all leaf nodes that perform 2D rendering
- G2DParentNode is a baseclass for the 2D compatible parent nodes
- IG2DNode is a common interface for G2DNode and G2DParentNode which should be used when traversing through the data structure.
- G2DSceneGraph G2DParentNode implementation containing event listeners and some extra features. This should always be the root component of a G2D scene graph datastructure.
Example G2DNode:
public class BackgroundNode extends G2DNode { private static final long serialVersionUID = -1L; protected Rectangle2D bounds = null; @SyncField("bounds") public void setBounds(Rectangle2D bounds) { this.bounds = bounds; } @Override public void render(Graphics2D g) { g.fill(bounds); } }
Because org.simantics.scenegraph is an independent scene graph datastructure, it can be rendered as such. Rendering can be performed by calling G2DSceneGraph.render(Graphics2D g2d) method. No changes are allowed to the scene graph while the structure is rendered (and rendering methods should not make any changes to the structure either, excluding transient node fields that are not synchronized between server and client).
3D
JOGL compatible implementation of the scenegraph can be found under package org.simantics.scenegraph.g3d.jogl in a separate plug-in org.simantics.scenegraph.g3d.
- JOGLNode
- JOGLParentNode
- JOGLPaintable
- JOGLSceneGraph
See org.simantics.scenegraph.g3d.eclipse.JOGLLocalView for an example how to use org.simantics.scenegraph with OpenGL.
Serialization
For serializing the scene graph datastructure there is class called org.simantics.scenegraph.remote.Serializer. The Serializer simply listens for any changes in the scene graph, and translates those changes into events which are then serialized and sent to ObjectOutputStream. To enable Serializer to listen for changes in the scene graph, the scene graph root node must be initialized with cglib. For this purpose there is a static method in Serializer:
G2DSceneGraph root = Serializer.newRemoteSceneGraphNode(G2DSceneGraph.class); Serializer serializer = new Serializer(root, input, output); serializer.start();
After this, the root node can be used normally. Any child added to it will be created by cglib automatically, hence Serializer works automatically with the whole scene graph data structure.
On the client side, Serializer can be directly attached to object streams, and it will create G2DSceneGraph object for you:
Serializer serializer = new Serializer(input, output); serializer.start(); G2DSceneGraph root = (G2DSceneGraph)serializer.getRoot();
Because the system works asynchronously, you should actually attach UpdateListener to serializer in real case.
Notice that you have to attach the serializer to the scene graph before any other operation. Serializer is an instance of PropertyChangeListener which will be set to listen the changes in the scene graph datastructure. If there are changes made before the serializer is attached, these changes will not be serialized.
Serializer uses ObjectInput and ObjectOutput to serialize and unserialize the scene graph, thus it can be used to write the scenegraph to a file and from a file, or to synchronize the scenegraph in a client-server system.
Using scenegraph
- Create scenegraph
G2DSceneGraph sg = new G2DSceneGraph(); sg.setRootPane(instanceOfJComponent); // In case you want to use swing components, you must set rootPane. // Register event handlers addKeyListener(sg); addMouseWheelListener(sg); addMouseListener(sg); addMouseMotionListener(sg); addFocusListener(sg);
- Create node
ShapeNode shape = sg.addNode(ShapeNode.class); shape.setShape(new Rectangle2D.Double(10, 10, 100, 100)); shape.setColor(Color.RED);
- Render
sg.render(g2d);
- Move shape
shape.setTransform(AffineTransform.getTranslateInstance(5, 5));
- Updating node from stateless object
ShapeNode shape = sg.getOrCreateNode("bounds", ShapeNode.class); shape.setShape(new Rectangle2D.Double(10, 10, 100, 100)); shape.setColor(Color.RED);
Scenegraph in Simantics
- Each canvas participant that wants to draw something should implement methods annotated with @SGInit and @SGCleanup
public class Participant extends AbstractCanvasParticipant { protected ShapeNode node = null; @SGInit public void initSG(G2DParentNode parent) { node = parent.addNode(ShapeNode.class); node.setZIndex(PAINT_PRIORITY); node.setShape(new Rectangle2D.Double(1, 1, 20, 20)); } @SGCleanup public void cleanupSG() { node.remove(); node = null; } }
- Optionally, you can specify a designation for @SGInit that specifies how you want your nodes to be transformed, i.e. in control space or in canvas space. The default value is canvas space. Use the scene graph viewer to understand how this affects your scene graph.
@SGInit(designation = SGDesignation.CONTROL)
- Each diagram element can implement interface SceneGraph to give their own contribution to the scene graph.
public class ElementSGNode implements SceneGraph { public static final Key SG_NODE = new SceneGraphNodeKey(Node.class, "SG_NODE"); @Override public void init(final IElement e, G2DParentNode parent) { ShapeNode node = (ShapeNode) e.getHint(SG_NODE); if (node == null) { node = parent.addNode(ShapeNode.class); e.setHint(ElementHints.KEY_SG_NODE, node); } node.setShape(new Rectangle2D.Double(1, 1, 20, 20)); } @Override public void cleanup(IElement e) { Node node = e.removeHint(SG_NODE); if (node != null) node.remove(); } }
org.simantics.scenegraph features
org.simantics.scenegraph package contains lots of readymade nodes and features. For a list of g2d nodes, see Scene graph node list.
Swing support
It is possible to integrate Swing components to 2D scenegraph by using org.simantics.scenegraph.g2d.nodes.swing.ComponentNode class. See for example MonitorNode or Trend2DNode for an example.
Eclipse view
org.simantics.scenegraph.eclipse package contains local and remote view of 2D and 3D scene graphs. You can try out these demos by executing the project as Eclipse Application and choosing the views "SceneGraph Remote View", "SceneGraph Local View", "JOGL SceneGraph Local View", "JOGL SceneGraph Remote View".
2D-3D integration
It is possible to create a G2DParentNode that works as a wrapper between 2D and 3D scenegraphs. This is not implemented yet.
Node Bounds
Scene graph nodes may either have specific spatial boundaries or have infinite boundaries. Nodes with infinite boundaries are omnipresent, i.e. should always be rendered. Node boundaries are necessary for doing things like spatial subdivision or picking.
Implementation
- Each node has a method getBoundsInLocal() which returns a Rectangle2D instance describing the 2D boundaries of the node in the element's local coordinate system. A null return value is interpreted as having infinite boundaries (omnipresence).
- Node base implementation classes have a method getBounds() that returns the node boundaries with local transforms applied.
- Nodes that have children (i.e., a parent node that does not draw anything) will return union of its childrens bounds.
- Bounds will need caching in order to be efficiently implemented in parent nodes. One option is to implement a mechanism for propagating transformation changes up in the node tree to keep parent nodes aware of transformation changes in their children. Possily implement a listener in parent nodes that can be registered into any transformable node.
Spatial Subdivision
In order to perform feasible visibility optimization during rendering some kind of spatial subdivision methods need to be employed. For example Trolltech QT GraphicsView [1] implements BSP-based partitioning to accelerate interaction and rendering in its 2D systems. Node boundaries are an essential requirement to performing this subdivision.
Implementation
- org.simantics.g2d.nodes.spatial contains RTreeNode that implements rectangular boundary -based subdivision of all of its direct child nodes.
- Rendering an RTreeNode includes performing a lookup into the R-Tree structure using the current clipping viewport. As a result, we will get the set of direct children that are visible within that viewport and therefore optimize the rendering process by quickly pruning what needs to be processed.
Node ⇔ ID mapping
In DOM implementations the client has the possibility to search for document nodes by their ID (getElementById(String)). Similarly scene graph nodes can be given identifications based on which they can be looked up from the scene graph through ILookupService which is implemented by scene graph root node G2DSceneGraph.
Normally the scene graph is a tree of nodes. Giving nodes scene graph -locally unique ID's through the lookup mechanism makes it possible to create links between nodes. The links effectively make the scene graph more like a DAG (Directed Acyclic Graph) or even a DG (Dependency Graph). Just as in DOM, the id attribute is not mandatory. Node lookup will only be possible if an ID is provided.
The lookup facility is available at the root node of the scene graph. Considering performance, having to always traverse to the root node to make lookups is a very likely bottleneck. This problem has been alleviated by using transient root node caches in ParentNode. This will naturally make the scene graph a bit more space consuming.
Using LookupService
- Giving a lookup ID to a node
G2DSceneGraph root = new G2DSceneGraph(); // Create new nodes Node child1 = root.addNode("node child 1", G2DParentNode.class); Node child2 = root.addNode("node child 2", G2DParentNode.class); // Give a lookup ID child2.setLookupId("child 2 lookup id"); // OR NodeUtil.map(child2, "child 2 lookup id");
- Performing a lookup
// ID -> INode INode lookedUpNode = NodeUtil.lookupNode(child1, "child 2 lookup id"); assert lookedUpNode == child2; //OR lookedUpNode = root.lookupNode("child 2 lookup id"); assert lookedUpNode == child2; // INode -> ID String lookedUpId = NodeUtil.lookupId(child2); assert lookedUpId.equals("child 2 lookup id"); // See NodeUtil for more utility methods for lookups and associations
Main interfaces
ParentNode
org.simantics.scenegraph.ParentNode
Add child node
Add node simply creates a new node object with the given class and sets it as a child of the parent node.
public final <TC> TC addNode(Class<TC> a); public <TC> TC addNode(String id, Class<TC> a);
Get or create node
If you are not sure if the node already exists, you should give a unique id for the node, and check if the node exists in the scene graph (actually you can only check if its a child of the parent node). For this purpose, there is a method called getOrCreateNode that simply checks if the the node exists and returns it if it does. Otherwise the method will create a new node using addNode.
public final <TC> TC getOrCreateNode(String id, Class<TC> a);
Remove whole subtree
To remove node and all its children from the scenegraph, a method called remove can be called. This removes node from the parent, and disposes itself and its children. After this method is called, the node and its children are disposed, and thus should not be used anymore.
public void remove();
Delete node
Delete method can be used to remove a single node from the scene graph tree, so that its children will still be part of the scene graph. This method basically moves nodes children under its parent, and disposes itself. After this method is called, only the node is disposed.
public void delete();
Remove all children
To remove nodes children, you can use removeNodes-method. This method removes and disposes all children of the node (and their children).
public final void removeNodes();
INode
org.simantics.scenegraph.INode
Append parent node
To add a node in the middle of the scene graph tree structure, you can use appendParent-method. Basically this method creates a new parent node, adds it as a child of the original parent. Then the node is removed from its parent and added under its new parent.
public <TC> TC appendParent(String id, Class<TC> nc);
Scenarios
Implementing a 2D node
- TODO: implementing your own G2DNode
- render, assumptions
- getBoundsInLocal
Hooking your node to UI events
By default, 2D nodes do not receive UI events. To receive events, do the following:
- Override int getEventMask(), use constants from EventTypes.
- Override init(), add addEventHandler(this)
- Override cleanup(), addremoveEventhandler(this)
- Override any of the following methods in G2DNode or G2DParentNode
- handleEvent
- handleKeyEvent, keyReleased, keyPressed
- handleCommand
- handleFocusEvent
- mouseButtonPressed, mouseButtonReleased, mouseClicked, mouseDoubleClicked
- mouseMoved, mouseDragged
- mouseEntered, mouseExited
- mouseWheelMoved
- handleTimeEvent
All event handler methods return boolean values where a true
return value indicates that the handler consumed the event and it should not be propagated further.
Picking
- TODO: picking
Coordinate system transformations
- TODO: global <-> local coordinate system transformations
Mouse capture/grabbing
- TODO: mouse capture