The Source for Java Technology Collaboration
User: Password:



Hans Muller's Blog

January 2008 Archives


Introducing the SceneGraph Project

Posted by hansmuller on January 08, 2008 at 06:50 PM | Permalink | Comments (16)

Introducing SceneGraph

I haven't written a blog entry since January when I advertised the fledgling Swing Application Framework (JSR-296) project with an uncharacteristically brief item. Work on that kept me busy until summer, when the Java FX juggernaut got underway here at Sun. Since then I've devoted much of my time to leading a project we've called "Scenario" that provides the graphical runtime for Java FX Script. We're now a public project on java.net called scenegraph.dev.java.net . You'll find downloads of the the 0.4.1 version of the Scenario source and binaries and (very) sketchy javadoc on the site. The 0.x version number is intended to convey the fact that the API hasn't stabilized yet. It's sufficient for experimentation and the implementation was robust enough to support a port of the Java FX Script interpreter, which we've showcased with a Scenario version of the FXPad Demo . Obviously, we don't recommend putting anything based on Scenario into production. Not yet.

The code is being made available under the GPLv2 license. Passion about open source licenses is not something I possess so I'll leave the shouting about the implications of this choice to others. Suffice it to say that, per my limited understanding of these matters, you're free to share the code, and in the process of developing it further. We're moving our discussions of the API to the newly minted Scene Graph java.net forum . If you're interested in the project's evolution, that would be a good place to start looking.

There are quite a few engineers working on Scenario, most of whom have made bigger contributions to the new software than I have, and you'll be hearing from them in their own blogs before too long. For now, what I'd like to do is to provide an introduction to the new Java APIs and just one demo. The team has written a whole raft of demos and we'll be opening up a subproject before too long, that contains the entire demo catalog.

Demo

All of the examples that follow are part of a demo, each one occupies a tab. If you press "control-T" after clicking on a demo, you'll get a nice interactive tree view of the scene graph's structure, thanks to Amy Fowler for that! So press the orange button to launch the demo.

Launch Button

Note also: the demo scales the selected example scene to fit. This is done with a small extension to JSGPanel, take a look at the code if you're interested.

Basics, Hello World

Intro Screenshot1

A scene graph, really a tree for now, is a data structure you create from leaf nodes that represent visual elements like 2D graphics and Swing components, filter nodes that represent visual state, like 2D transforms and composition, and group nodes that manage a list of children. All nodes are subclasses of SGNode and have a parent node, rectangular bounds, visibility, and support for event dispatching. Leaf nodes extend SGLeaf and have paint/repaint methods similar to AWT/Swing. SGFilter nodes have one child, and SGGroups have a list of them. To display a scene graph you set the scene property of a JSGPanel, and add the panel to a Swing application in the usual way. Here's a simple example:

SGText text = new SGText();
text.setText("Hello World");
JSGPanel panel = new JSGPanel();
panel.setScene(text);
panel.setPreferredSize(new Dimension(640, 480));
// create a JFrame, add the panel to it, etc..
    

One unusual line from the previous example, from a Swing programmer's perspective, is that we've explicitly set the preferred size of the JSGPanel. Although the JSGPanel will compute (and recompute...) a preferred size based on bounds of its scene, it's usually a good idea to define a reasonable fixed preferred size instead. To make the scene slightly more interesting to look at, we can set the SGText node's font and color, turn on antialiasing, and configure the panel's background color:

SGText text = new SGText();
text.setText("Hello World");
text.setFont(new Font("SansSerif", Font.PLAIN, 36));
text.setAntialiasingHint(RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
text.setFillPaint(Color.WHITE);
JSGPanel panel = new JSGPanel();
panel.setBackground(Color.BLACK);
panel.setScene(text);
panel.setPreferredSize(new Dimension(640, 480));
    

More Hello World: Groups and Shapes

Intro Screenshot2

Most of the Scenario classes are simple Java Beans. They provide null (no parameters) constructors and mutable properties. The API is intended to be "minimal" in the sense that it exposes the capabilities of the underlying Java 2D and Swing classes but does attempt to substantially simplify or abstract them. Scenario is intended to serve as the basis for higher level abstractions that require a scene graph, notably Java FX Script. For that reason, you may find programming against the Scenario API directly to be a bit tedious. Creating your own abstractions that simplify creating scene graphs is definitely the order of the day.

To add a border to the simple Hello World scene, we'll write a method that takes any scene graph node and puts a garish rounded rectangle border behind it. This example demonstrates using the SGShape leaf node which can render any Java 2D Shape , like lines, arcs, bezier curves, and of course rounded rectangles. Just to highlight the support for both filling and stroking (drawing the outline of) shapes, we'll do both here. The createBorder method creates a red and yellow rounded rectangle that's a little bigger than the node it's given. It returns an SGGroup scene graph node that contains both the original node and the "border".

SGNode createBorder(SGNode node) {
    Rectangle2D nodeR = node.getBounds();
    double borderWidth = 10;
    double x = nodeR.getX() - borderWidth;
    double y = nodeR.getY() - borderWidth;
    double w = nodeR.getWidth() + (2 * borderWidth);
    double h = nodeR.getHeight() + (2 * borderWidth);
    double a = 1.5 * borderWidth;
    SGShape border = new SGShape();
    border.setShape(new RoundRectangle2D.Double(x, y, w, h, a, a));
    border.setFillPaint(new Color(0x660000));
    border.setDrawPaint(new Color(0xFFFF33));
    border.setDrawStroke(new BasicStroke(borderWidth / 2.0));
    border.setMode(SGShape.Mode.STROKE_FILL);
    border.setAntialiasingHint(RenderingHints.VALUE_ANTIALIAS_ON);
    SGGroup borderedNode = new SGGroup();
    borderedNode.add(border);
    borderedNode.add(node);
    return borderedNode;
}
    

Adding a the border doesn't change the code that creates the scene graph very much:

SGText text = new SGText();
text.setText("Hello World");
// same as before ...
panel.setScene(createBorder(text));
    

Rotating Hello World: Transforms

Intro Screenshot3

As you can see in the previous example, the SGNode#getBounds() method returns a bounding box for the its node, in the way same way Component#getBounds() does for AWT and Swing components. In AWT and Swing, a component's parent node recursively defines its origin in terms of a translation, which is the value of getParent().getLocation(). Scene graphs are much more flexible than that. The relationship between a child and its parent node can be defined with any 2D affine transformation.

To rotate a scene graph node around a point, you have to assemble a chain of three transforms that: translate the node so that the rotation point is at the origin, rotate the desired amount, translate the node back to its original location. Transforms are created with SGTransform nodes, which are SGFilter subclasses because they have just one child, which is the node the transform is to be applied to. Here's a method that creates such a chain and uses the node's bounds' center as the rotation point:

SGTransform createRotation(SGNode node) {
    Rectangle2D nodeR = node.getBounds();
    double cx = nodeR.getCenterX();
    double cy = nodeR.getCenterY();
    SGTransform toOriginT = SGTransform.createTranslation(-cx, -cy, node);
    SGTransform.Rotate rotateT = SGTransform.createRotation(0.0, toOriginT);
    return SGTransform.createTranslation(cx, cy, rotateT);
}
    

To use the createRotation method we apply it to the node that's going to spin around its center, and then add the returned value to the scene instead of the node itself. The return value is the chain of three transform nodes followed by the original node. To specify a rotation you have to refer to the second SGTransform.Rotate from the chain and change its rotation property:

SGTransform scene = createRotation(node);
SGTransform.Rotate rotateT = (SGTransform.Rotate)scene.getChild();
rotateT.setRotation(...);
    

Images and More Layout

Intro Screenshot4

Images can be incorporated in a scene graph with the SGImage node type. To add one to our scene so that it appears to the right of the "Hello World" text, we'll have to create an SGGroup node that contains the text and the image, and then use SGTransform nodes to arrange the group's children along a row. Here's a method that creates such a group:

SGNode createRow(SGNode... nodes) {
    double rowHeight = 0.0;
    for(SGNode node : nodes) {
        rowHeight = Math.max(rowHeight, node.getBounds().getHeight());
    }
    SGGroup row = new SGGroup();
    double x = 0.0;
    double gap = 8.0;
    for(SGNode node : nodes) {        
        Rectangle2D nodeR = node.getBounds();
        double y = (rowHeight - nodeR.getHeight()) / 2.0;
        double dx = x - nodeR.getX();
        double dy = y - nodeR.getY();
        SGTransform xlate = SGTransform.createTranslation(dx, dy, node);
        row.add(xlate);
        x += nodeR.getWidth() + gap;
    }
    return row;
}
    

The code to create the SGImage node and the overall scene looks like this:

SGNode createEarth() {
    BufferedImage image = null; 
    // ... code to load the image file
    SGImage sgImage = new SGImage();
    sgImage.setImage(image);
    return sgImage;
}
// ...
SGNode row = createRow(createHelloWorldText(), createEarth());
SGNode scene = createBorder(row);
JSGPanel panel = new JSGPanel();
panel.setScene(scene);
    

Handling Input

Intro Screenshot5

All nodes can handle mouse and keyboard events and the support for doing so is very similar to what AWT/Swing provides. For example to make a node handle mouse events you add an SGMouseListener. SGMouseListener combines the methods from AWT's MouseListener and MouseMotionListener and it adds an SGNode argument to each one. SGKeyListener and SGFocusListener are similar. The source of a scene graph mouse or keyboard event is always a JSGPanel and the additional node argument indicates the node the event was actually dispatched to.

In this example, we've added a mouse listener to the earth node in the scene from the previous example and restored the support for rotating the scene. Dragging the earth with the mouse rotates the scene. Here's the code that sets up the scene and its SGMouseListener:

SGNode earth = createEarth();
SGNode row = createRow(createHelloWorldText(), earth);
SGTransform scene = createRotation(createBorder(row));
final SGTransform.Rotate rotateT = (SGTransform.Rotate)scene.getChild();
SGMouseListener changeRotation = new SGMouseAdapter() {
    @Override public void mouseDragged(MouseEvent e, SGNode node) {
        Rectangle2D r = e.getComponent().getBounds();
        double x = e.getX() - r.getCenterX();
        double y = e.getY() - r.getCenterY();
        rotateT.setRotation(Math.atan2(y, x));
    }
};
earth.addMouseListener(changeRotation);
JSGPanel panel = new JSGPanel();
panel.setScene(scene);
    

Although there's a great deal more to say by way of introducing Scenario, this blog entry has grown long enough that I'd better just take the same tack that Chet did , and declare this "Part 1".

The code for the examples can be found here: http://weblogs.java.net/blog/hansmuller/archive/Intro.java A easy to build version will appear along with the other Scenario demos, later this month.





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds