Thursday, March 8, 2007

More Java3D and Groovy Builders

In the previous post, a basic Builder for Java3D was shown. For this post I've decided to take one another example from the Java3D distribution and try to implement the scene creation code by using the builder. Also I've received nice suggestions that and I'm going to implement.


The example that I'm going to use is SphereMotion.java which is a nice demonstration of spheres, animation and lights.

The scene creation code of this example can be found in the createSceneGraph method of the SphereMotion class in SphereMotion.java. I'm not going to show the code in this post.

This example contains the following elements:


  1. Uses the Background class to set the background color.

  2. Creates a big Sphere which is the central element of the scene

  3. Conditionally creates a PointLight, DirectionalLight or SpotLight for the Sphere illumination.

  4. Uses a PositionInterpolator and RotationInterpolator to perform an animations

  5. Uses AmbientLight for general scene illumination.




Helpers for Background, Sphere, PointLight, DirectionalLight, SpotLight, PositionInterpolator and AmbientLight were created.

According to the suggestions, we can use Groovy array literals to specify attribute values that require classes. For example instead of using "alpha: new Alpha(-1,4000)" use "alpha: [-1,4000]". This makes the code more readable.

The flexibility of having the control over attribute and element creation let's you support many ways of doing the same thing. For example, I wanted to allow the "position" attribute of the PointLight class to be specified as an instance of the Point3f class(for example "position:new Point(0.0f,0.0f,0.0f)) or as an array of three elements (for example "position:[0.0f, 0.0f, 0.0f]"). To implement this in the builder, the PointLightHelper code for the position attribute looks like this:


public class PointLightHelper extends LightHelper {

protected void applyAttributes(Node aNode,
Map attributes) {
super.applyAttributes(aNode, attributes);
PointLight pl = (PointLight)aNode;

// Check the position attribute
if (attributes.get("position") != null) {
Object value = attributes.get("position");
if (AttributeHelper.isArrayList3fTuple(value)) {
pl.setPosition(
AttributeHelper.arrayListToPoint3f(
(ArrayList)value));
} else
if (value instanceof Point3f) {
pl.setPosition((Point3f)value);
}
}
...


where


public static Point3f arrayListToPoint3f(ArrayList a) {
Vector3f result;
return
new Point3f(
((Number)a.get(0)).floatValue(),
((Number)a.get(1)).floatValue(),
((Number)a.get(2)).floatValue());
}

public static boolean isArrayList3fTuple(Object value) {
boolean result = false;
if (value instanceof ArrayList &&
((ArrayList)value).size() == 3 ) {
ArrayList a = ((ArrayList)value);
result =
a.get(0) instanceof Number &&
a.get(1) instanceof Number &&
a.get(2) instanceof Number;
}
return result;
}




The TransformGroupHelper class was also modified so more specific transformations can be specified. For example a I wanted to create a transform group to rotate on Y axis only. To solve this an attribute rotY(and rotX and rotZ) was created to support this directly on the TransformGroupHelper. This attribute was implemented as:



if (key.matches("rot(X|Y|Z)") &&
((value instanceof Number) )) {

double dValue = ((Number)value).doubleValue();

Transform3D theRotationTransform =
new Transform3D();
tg.getTransform(theRotationTransform);

switch(Character.toUpperCase(
key.charAt(key.length() - 1))) {
case 'X':
theRotationTransform.rotX(dValue);
break;
case 'Y':
theRotationTransform.rotY(dValue);
break;
case 'Z':
theRotationTransform.rotZ(dValue);
break;
}
tg.setTransform(theRotationTransform);
}




Once having the helpers for the new elements implemented, the only thing missing to resolve was to translate the scene creation code to the builder. One of the major issues was the creation of the Light object. In the original code, the Light was created like this:



switch (lightType) {
case DIRECTIONAL_LIGHT:
lgt1 = new DirectionalLight(lColor1, lDirect1);
lgt2 = new DirectionalLight(lColor2, lDirect2);
break;
case POINT_LIGHT:
lgt1 = new PointLight(lColor1, lPoint, atten);
lgt2 = new PointLight(lColor2, lPoint, atten);
break;
case SPOT_LIGHT:
lgt1 = new SpotLight(lColor1, lPoint, atten, lDirect1,
25.0f * (float)Math.PI / 180.0f, 10.0f);
lgt2 = new SpotLight(lColor2, lPoint, atten, lDirect2,
25.0f * (float)Math.PI / 180.0f, 10.0f);
break;
}

...

l1Trans.addChild(lgt1);
l2Trans.addChild(lgt2);




This means that the creation of the instance of the desired light is a parameter of the program. Also another issue was that two lights were created and different parents are assigned to each one. In order to support this we split the creation of both lights into two different switch statements.

The final code for the creation of this scene looks like this:



public BranchGroup createSceneGraph() {
Color3f eColor = new Color3f(0.0f, 0.0f, 0.0f);
Color3f sColor = new Color3f(1.0f, 1.0f, 1.0f);
Color3f objColor = new Color3f(0.6f, 0.6f, 0.6f);
Color3f lColor1= new Color3f(1.0f, 0.0f, 0.0f);
Color3f lColor2= new Color3f(0.0f, 1.0f, 0.0f);
Color3f alColor= new Color3f(0.2f, 0.2f, 0.2f);
Color3f bgColor= new Color3f(0.05f, 0.05f, 0.2f);

TransformGroup t1;
TransformGroup t2;

Java3dBuilder j3b = new Java3dBuilder()

BoundingSphere bounds =
new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0);

// Create the root of the branch graph
BranchGroup objRoot =
j3b.branchGroup() {
transformGroup(scale:0.4) {
background(color:new Color3f(0.05f, 0.05f, 0.2f),bounds:bounds)
sphere(radius:1.0f,
flags:Sphere.GENERATE_NORMALS,
divisions:80,
material:new Material(objColor,
eColor,
objColor,
sColor,
100.0f),
lighting:true
)
t1 = transformGroup(
capability:TransformGroup.ALLOW_TRANSFORM_WRITE){
transformGroup(
translate:[0.0, 0.0, 2.0])
{
sphere(radius:0.05f,coloringAttributes:lColor1)
switch(lightType) {
case DIRECTIONAL_LIGHT:
directionalLight(color:lColor1,
direction:[0.0, 0.0, -2.0],
influencingBounds:bounds)
break;
case POINT_LIGHT:
pointLight(color:lColor1,
position:[0, 0, 0],
attenuation:[1,0,0],
influencingBounds:bounds)
break;
case SPOT_LIGHT:
spotLight(color:lColor1,
position:[0, 0, 0],
attenuation:[1,0,0],
direction:[0.0, 0.0, -2.0],
spreadAngle:25.0f * (float)Math.PI / 180.0f,
concentration:10.0,
influencingBounds:bounds)
break;
}
}
}
t2 = transformGroup(
capability:TransformGroup.ALLOW_TRANSFORM_WRITE){
transformGroup(
translate:[0.5, 0.8, 2.0])
{
sphere(radius:0.05f,coloringAttributes:lColor2)
switch(lightType) {
case DIRECTIONAL_LIGHT:
directionalLight(color:lColor2,
direction:[-0.5, -0.8, -2.0],
influencingBounds:bounds)
break;
case POINT_LIGHT:
pointLight(color:lColor2,
position:[0, 0, 0],
attenuation:[1,0,0],
influencingBounds:bounds)
break;
case SPOT_LIGHT:
spotLight(color:lColor2,
position:[0, 0, 0],
attenuation:[1,0,0],
direction:[-0.5, -0.8, -2.0],
spreadAngle:25.0f * (float)Math.PI / 180.0f,
concentration:10,
influencingBounds:bounds)
break;
}
}
}
ambientLight(color:alColor,
influencingBounds:bounds)
rotationInterpolator(alpha:new Alpha(-1, Alpha.INCREASING_ENABLE,
0, 0,
4000, 0, 0,
0, 0, 0),
target:t1,
axisOfTransform:new Transform3D(),
schedulingBounds:bounds,
minAngle:0.0f,
maxAngle:Math.PI*2.0f);
rotationInterpolator(alpha:new Alpha(-1, Alpha.INCREASING_ENABLE,
0, 0,
1000, 0, 0,
0, 0, 0),
target:t2,
axisOfTransform:new Transform3D(),
schedulingBounds:bounds,
minAngle:0.0f,
maxAngle:0.0f);
positionInterpolator(
alpha:new Alpha(-1,
Alpha.INCREASING_ENABLE |
Alpha.DECREASING_ENABLE,
0, 0,
5000, 0, 0,
5000, 0, 0),
target:univ.getViewingPlatform().getViewPlatformTransform(),
yAxisOfTransform:(-1*Math.PI/2.0),
schedulingBounds:bounds,
start:2.0f,
end:3.5f);


}
}
objRoot.compile();

return objRoot;
}





The result is the same as the Java example:




If someone wants to look at the code of this experiment, it can be found here.

In future posts I'm going to try to implement more Java3D functionality into the builder.