Friday, February 23, 2007

Using Groovy Builders to create Java3D scenes

While listening to Java Posse episode 101 I found out about a nice feature of the Groovy language called Builders.

Groovy Builders provide a unified syntax for tree-like structures. The following example shows the use of the MarkupBuilder to create an XML document.


import groovy.xml.*;

writer = new StringWriter()
builder = new MarkupBuilder(writer)

builder.document(name:"a document") {
chapter(name:"First one") {
page1("first page")
page2("second page")
}
chapter(name:"First two") {
page1("page 1")

page2("page 2" )

}
}

println(writer.toString())



This program generates the following output:


<document name='a document'>
<chapter name='First one'>
<page1>first page</page1>
<page2>second page</page2>
</chapter>
<chapter name='First two'>
<page1>page 1</page1>
<page2>page 2</page2>
</chapter>
</document>


The nice thing about Groovy builders is that can also be used to create other kinds of tree structures such as Swing GUIs(SwingBuilder) or Ant tasks(AntBuilder) among others. For example, using the Swing builder you can create a simple form:



import java.awt.*
import javax.swing.*
import groovy.swing.SwingBuilder


sb = new SwingBuilder()

aFrame = sb.frame(title:"Hello World",size:[200,200]){
panel(layout: new FlowLayout()) {
scrollPane(preferredSize:[200,130]) {
tree()
}
panel(layout:new GridLayout(1,2,15,15)){
button(text:"Ok")
button(text:"Cancel")
}
}
}
aFrame.show()



Will generate:




All this is possible because Groovy provides an extensible factory mechanism for this syntax. I think this makes Groovy builders a much better feature than having a just XML literals (like in VB9) because it gives the developer freedom to select other data formats (objects, JSON,binary files, S-Expressions!,etc).

In other to create a new builder you have to inherit from groovy.util.BuilderSupport and implement the following methods:



protected abstract void setParent(Object parent, Object child);
protected abstract Object createNode(Object name);
protected abstract Object createNode(Object name, Object value);
protected abstract Object createNode(Object name, Map attributes);
protected abstract Object createNode(Object name, Map attributes, Object value);



The createNode methods are used to create one node of the tree with the given name,attributes and value. The setParent method is used to connect parent and child nodes.

In this post I'm going to create the first approach of a new builder for Groovy that can be used to build Java3D scenes.

Java3D scenes are represented as a scene graph. The objects in the scene graph can be arranged in a tree structure (although references between sibling nodes can exists, hence the name). For example:



This tree represents a scene with tree objects two spheres (sp) and one cube (cb) and one transformation group (tg) and a branch group (bg). Transformation groups, as the name says, applies a transformation (rotate,scale,etc) to all of its child elements. Java3D is a huge topic which is not intended to be covered in this post, for more information check here and here.


In this post I'll only cover a couple of Java3D classes, in future posts in I'm going to try to add more elements to the Groovy builder.

The main strategy is to create a new builder class (Java3dBuidler) which creates Java3D nodes by using helpers that map attributes to Node properties and constructor arguments. This mapping is done using helpers for each node. All the code for the builder support will be created using Java although Groovy could also be used.

First of all we create the Java3dBuilder class:


public class Java3dBuilder extends BuilderSupport {

HashMap<String,J3dNodeHelper> objects;

public Java3dBuilder() {
objects = new HashMap();

objects.put("branchGroup",new BranchGroupHelper());
objects.put("transformGroup",new TransformGroupHelper());
objects.put("colorCube",new ColorCubeHelper());
objects.put("rotationInterpolator",new RotatorInterpolatorHelper());
}
...

The Java3dBuilder constructor creates a map between the names of the elements to be used in the scene and the node creation helpers.

The J3dNodeHelper is the base class of the node creation helpers which are used to create instances of individual kinds of Java3D nodes. For example BranchGroupHelper will be used to create BranchGroup instances using the "branchGroup" name.

The setParent method as described above, creates a link between parent and child. In this case a parent must be a Java3D Group and the child a Java3D Node.


protected void setParent(Object parent, Object child) {
if (parent instanceof Group ) {
Group gParent = (Group)parent;
Node nChild = (Node)child;
gParent.addChild(nChild);
}
}


The createNode methods use the required node helper to create the required node .


protected SceneGraphObject createObject(String name) {
J3dNodeHelper c = objects.get(name);
return (SceneGraphObject)c.create(new HashMap());
}
protected SceneGraphObject createObject(String name,Map attributes) {
J3dNodeHelper c = objects.get(name);
return (SceneGraphObject)c.create(attributes);
}

protected Object createNode(Object name, Map atts, Object value) {
SceneGraphObject result = null;
String sName = (String)name;
SceneGraphObject current = (SceneGraphObject)getCurrent();
result = createObject(sName,atts);
return result;
}

protected Object createNode(Object object, Map atts) {
SceneGraphObject result = null;
String sName = (String)object;
return createObject(sName,atts);
}

protected Object createNode(Object name, Object value) {
SceneGraphObject result = null;
SceneGraphObject current = (SceneGraphObject)getCurrent();
String sName = (String)name;
result = createObject(sName);
return result;
}

protected Object createNode(Object name) {
SceneGraphObject result = null;
SceneGraphObject current = (SceneGraphObject)getCurrent();
String sName = (String)name;
result = createObject(sName);
return result;
}






The J3dNodeHelper class is implemented as:



public abstract class J3dNodeHelper {
public J3dNodeHelper() {
}

abstract protected Node createNode();

protected void applyAttributes(Node aNode,Map attributes) {
}

public Node create(Map attributes) {
Node result = createNode();
applyAttributes(result,attributes);
return result;
}
}




An example of a J3dNodeHelper used for TransformGroup is the following:



public class TransformGroupHelper extends J3dNodeHelper{
protected void applyAttributes(Node aNode,Map attributes) {
TransformGroup tg = (TransformGroup)aNode;
Set<Map.Entry<Object,Object>> s = attributes.entrySet();
for (Map.Entry<Object,Object> e : s) {
if (e.getKey().toString().equals("capability") &&
e.getValue() instanceof Integer) {
tg.setCapability(((Integer)e.getValue()).intValue());
} else
if (e.getKey().toString().equals("transform") &&
e.getValue() instanceof Transform3D) {
tg.setTransform(((Transform3D)e.getValue()));
}else {
System.out.println("Could not set property "+e.getKey()+
" ,"+e.getValue()+","+e.getValue().getClass());
}
}
}
protected Node createNode() {
return new TransformGroup();
}
}




The TransformGroupHelper class maps two TransformGroup attributes: capability and transform.

Also there're some cases where attributes have to be mapped to constructor arguments, because of properties that cannot be changed after object creation. In the case the create method must extract this values before creating the actual node . For example the helper for ColorCube class looks like this:



public class ColorCubeHelper extends J3dNodeHelper {
protected Node createNode() {
return new ColorCube();
}
protected Node createNode(double scale) {
return new ColorCube(scale);
}

public Node create(Map attributes) {
Node retValue;
if (attributes.get("scale") != null) {
retValue =
createNode(
((BigDecimal)attributes.get("scale")).doubleValue());
} else {
retValue = createNode();
}
applyAttributes(retValue,attributes);
return retValue;
}

}



Now that we have all the infrastructure ready, we can start using our builder. The following example show the creation of the scene graph using our builder:



public BranchGroup createSceneGraph() {
Java3dBuilder j3b = new Java3dBuilder()
BranchGroup objRoot ;
objRoot =
j3b.branchGroup() {
transformGroup(
transform:new Transform3D(
new Quat4f(
3.4f,
4.0f,
1.5f,
1.0f),
new Vector3d(),
1.0)) {
colorCube(scale:0.5)
}
};
objRoot.compile();
return objRoot;
}



Which generates the scene:



The code for scene creation of the HelloUniverse.java, included with Java3D, looks like this (removing the comments):



public BranchGroup createSceneGraph() {
BranchGroup objRoot = new BranchGroup();

TransformGroup objTrans = new TransformGroup();
objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
objRoot.addChild(objTrans);

objTrans.addChild(new ColorCube(0.4));

Transform3D yAxis = new Transform3D();
Alpha rotationAlpha = new Alpha(-1, 4000);

RotationInterpolator rotator =
new RotationInterpolator(rotationAlpha, objTrans, yAxis,
0.0f, (float) (Math.PI*2.0f));
BoundingSphere bounds =
new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0);
rotator.setSchedulingBounds(bounds);
objRoot.addChild(rotator);

objRoot.compile();

return objRoot;
}




If we want to create this scene using our builder we write:



public BranchGroup createSceneGraph() {
TransformGroup tg;
Java3dBuilder j3b = new Java3dBuilder()
BranchGroup objRoot ;

objRoot =
j3b.branchGroup() {
tg = transformGroup(
capability:TransformGroup.ALLOW_TRANSFORM_WRITE) {
colorCube(scale:0.4)
};
rotationInterpolator(
alpha:new Alpha(-1,4000),
target:tg,
axisOfTransform:new Transform3D(),
minAngle:0.0,
maxAngle:Math.PI*2.0,
schedulingBounds:new BoundingSphere(
new Point3d(0.0,0.0,0.0), 100.0)
);
}

objRoot.compile();
return objRoot;
}



More complex scenes can be created, for example:



public BranchGroup createSceneGraph() {
TransformGroup tg;
Java3dBuilder j3b = new Java3dBuilder()

BranchGroup objRoot;
objRoot =
j3b.branchGroup() {
tg = transformGroup(
capability:TransformGroup.ALLOW_TRANSFORM_WRITE) {
colorCube(scale:0.1)
transformGroup(transform:new Transform3D(
new Quat4f(),
new Vector3d(0.0f,0.5f,0.5f),
1.0)) {
colorCube(scale:0.1)
}
transformGroup(transform:new Transform3D(
new Quat4f(),
new Vector3d(0.0f,-0.5f,0.5f),
1.0)) {
colorCube(scale:0.1)
}
transformGroup(transform:new Transform3D(
new Quat4f(),
new Vector3d(0.5f,0.0f,0.5f),
1.0)) {
colorCube(scale:0.1)
}
transformGroup(transform:new Transform3D(
new Quat4f(),
new Vector3d(-0.5f,0.0f,0.5f),
1.0)) {
colorCube(scale:0.1)
}
};
rotationInterpolator(
alpha:new Alpha(-1,4000),
target:tg,
axisOfTransform:new Transform3D(),
minAngle:0.0,
maxAngle:Math.PI*2.0,
schedulingBounds:new BoundingSphere(
new Point3d(0.0,0.0,0.0), 100.0)
);
}

objRoot.compile();
return objRoot;
}





Will generate:



In future posts I'm going to elaborate more on this topic. In this post only four Java3D Node classes were used, more helpers need to be created in order to support Materials, Lights, Primitives,etc.

Also in the future it'll be very interesting to see what F3 is doing for Swing/Java2D and soon for 3D graphics.

7 comments:

Dierk said...

How cool! Congratulations to such a wonderful usage of the builder concept and the excellent description.

In some cases it could be even more idomatic if you want, e.g.
alpha:new Alpha(-1,4000)
could possibly be
alpha: [-1, 4000]
.

Thanks for this excellent blog post
Dierk

tog said...

Hey that's cool, I am already using both Java3D and Groovy in our OpenSource software. Thanks to your post, I will be able to go further :-)
tog

Charles Oliver Nutter said...

So am I understanding right: you have to write the builder in Java?

Jochen "blackdrag" Theodorou said...

very nice! I programmed in Java3d several years ago and I hated the scene graph so much ;)

Btw, in your example with the four cubes, you could do something crazy as:
def coords = [[0.0f,0.5f],[0.0f,-0.5f],[0.5f,0.0f],[-0.5f,0.0f]]
coords.each {
transformGroup(transform:new Transform3D(new Quat4f(), new Vector3d(*it,0.5f), 1.0)) {
colorCube(scale:0.1)
}
}
or something like:
float x=0,y=0
def coords = [{y=0.5},{y=-y},{x=-y;y=0},{x=-x}]
coords.each {
it.call()
transformGroup(transform:new Transform3D(new Quat4f(), new Vector3d(x,y,0.5f), 1.0)) {
colorCube(scale:0.1)
}
}

both versions would create the 4 cubes, but o course this is a bit less readable. Btw, how about a default Quat4f, that is empty and then doing something like:

transformGroup(0.0f,0.5f,0.5f,1.0) {
colorCube(scale:0.1)
}

or even:

transformGroup(0.0f,0.5f,0.5f) {
colorCube(scale:0.1)
}

well, very different variants are possible ;)

Donald said...

So am I understanding right: you have to write the builder in Java?

You can use plain Groovy as well.

Luis Diego Fallas said...

The builder could also be written in Groovy or any JVM language that let's you create inherit from a
groovy.util.BuilderSupport.

A nice experiment will be to implement the Builder in another language that makes the attribute extraction and node creation easer.

durp said...

I re-wrote and extended this fine seminal work in pure Groovy. I was amazed at how much cleaner and concise the implementation became (shouldn't have been :) Anyone interested in me sharing?