The Java 3D API allows you to develop 3D graphics applications that have a high degree of visual realism. Since version 1.2, Java 3D is developed under the Java Community Process. Java 3D runs on top of OpenGL or Direct3D; it’s also an interface that encapsulates graphics programming using a real, object-oriented concept. In addition, Java 3D offers extensive 3D sound support.
This article describes a scene that is constructed using a scene graph, which is a representation of the objects that have to be shown. This scene graph is structured as a tree containing several elements that are necessary to display the objects.
3D world mechanisms
The design of Java 3D is significantly different from popular 3D graphics APIs such as OpenGL and Direct3D, which are low-level procedural APIs that are closely tied to the design of 3D hardware. With Java 3D, you set up all your graphics objects (also called geometry objects) in a scene graph, which is a hierarchical model containing all the information about the objects in your scene and how they will be rendered. Then, you hand the scene graph over to Java 3D for rendering. You don’t have to write any code to handle displaying your data — Java 3D does it for you. You get to program at a higher level with the many built-in power tools.
Java 3D can take advantage of any 3D acceleration that your graphics adapter provides. Java 3D ultimately generates OpenGL calls in a JNI layer that can be accelerated by your graphics card. OpenGL accelerated adapters are common in newer workstations, so your Java 3D programs should be hardware accelerated.
In a three-dimensional coordinate (x,y,z), the z component specifies distance from the viewer. Java 3D uses z values to remove nonvisible surfaces of distance obscured objects. The z values of the red torus in Figure A are small because it is close to the viewer. It will obscure portions of the blue torus when the z values of both tori are compared during rendering.
Figure A
3D objects
A 3D object contains a collection of coordinates rendered together. The Primitive class is an abstract class for geometry objects that can be used as simple building blocks in your scene graph. Java 3D includes four useful concrete subclasses of Primitive — Sphere, Box, Cone, and Cylinder — that allow you to easily create basic objects without having to specify a lot of data. For example, when using the Sphere class, you simply specify a radius, and all the vertex data is generated for you. If you are not using one of the Primitive classes, you’ll have to use the Shape3D class to specify all of the vertex data. You can specify data as triangles, quadrilaterals, lines, and points. A geometric representation of a sphere would be defined as a polygonal mesh, typically using strips of connected triangles or quads.
At a minimum, every vertex must have a location value (coordinate). In addition to location values, you can specify other items for each vertex, such as a color value, normal vector, and texture coordinates. Normal vectors are used for lighting effects, and texture coordinates are used when applying textures to the surface via texture mapping. Each vertex could also have an alpha, or transparency, value specified with its color. When you use the Primitive classes, vertex normals and texture coordinates are generated for you.
While you can specify a great deal of data with each vertex, many of your graphics effects are applied using the Appearance object. This object describes the overall attributes of an object’s surface. Each Shape3D and Primitive object will have its own Appearance object, and each Appearance object contains several attribute objects. For example, an Appearance object can contain both a ColoringAttributes object and a RenderingAttributes object.
Transformations, lightning, and texturing
When the objects are ready and you want them to be displayed, you can move and scale them by using 3D transformations — in essence, animating the objects. The location, direction, and orientation of your view is called the viewpoint. Transformations are specified as matrices in the powerful Transform3D class. Transform3D has many helper functions for specifying common transformations, such as translations, rotations, and scaling.
The setTranslation(Vector3f trans) method translates (moves) an object by replacing the translate values of this transform with the x, y, and z values in the trans argument. The setScale(double scale) method sets the scale of this transform. You should use this function to resize an object. The rotX(double angle) method sets the rotational component to a counterclockwise rotation around the X axis.
In addition to specifying what objects appear in your scene, you can also control how they appear by specifying lighting effects. You can specify the type of lighting effect, like a spotlight, and the color of the light. You can also apply fog effects to your scene and set up automated behaviors of your objects.
Texture mapping (commonly referred to as wallpapering) is used to provide more realism to a scene. For instance, you can apply a wood grain image on an object’s surface to simulate an oak table top. To use texture mapping, you need to specify the image, where to paste it on the object, and what to do if the image doesn’t fit quite right, such as when applying a rectangular bitmap to a non-rectangular polygon. Java 3D has simplified the process of loading texture images. The TextureLoader class is in the Java 3D utility classes.
TextureLoader texLoader = new TextureLoader(url,imageobserver) appearance.setTexture(texLoader.getTexture());
You should always set the width and height of your texture image to a power of 2 for a realistic look. Other values will cause TextureLoader to squish the texture.
Lights are used to illuminate the geometry objects in your scene. There are four types of lights, which are all subclasses of the abstract Light class. All lights have a color value, an on/off bit, and a bounding object that describes what areas of the scene it illuminates. In the real world, the objects around you are lit by several light sources. For instance, sunlight and an overhead light will affect the color and appearance of objects in a room. In Java 3D, you can simulate realistic lighting effects by using multiple light sources. You can use the following types of light in your application:
- An AmbientLight is everywhere in the scene. It does not originate from a particular point, and it does not point in a particular direction.
- A PointLight radiates from a specified location in all directions and diminishes with distance. An example of a point light is a desk lamp with no lampshade.
- A SpotLight is a type of point light that restricts the light to a cone shape. An example of a spot light is a flashlight.
- A DirectionalLight shines in a particular direction but doesn’t emanate from any particular location. All light rays of a directional light travel in parallel. The sun is technically a point light source, but sunlight can be more accurately imitated using a DirectionalLight.
The teapot in Figure B has ambient light (you can see that the back side is illuminated slightly), and directional light shining on the front. Both lights affect the final color of each triangle on the surface of the teapot.
Figure B
Material properties
Material properties describe how an object reflects light. If your object (Primitive or Shape3D) does not have a Material object in its Appearance object, it will not be illuminated even though you are specifying a light source. You must create a Material object, enable lighting in the Material object, and add the Material object to the Appearance object. The Material object is one of several attribute sets that are held in the Appearance object.
To better understand how material properties affect an object’s appearance, think about a shiny ruby gemstone object and a red carpet. While they are both red, they will reflect light differently — the ruby will have a bright highlight where the light bounces off it, and the carpet will appear to scatter the light. To specify this difference in appearance to Java 3D, you’d give the ruby a high shininess value in the Material object and the carpet a very low shininess value.
A surface normal is a vector that is perpendicular to the surface at the vertex, representing the orientation of the surface at that vertex. The surface normal affects how light is reflected from a surface when calculating lighting effects. The surface normals and position of the viewer determine where the shiny highlight (or specular reflection) is located on a sphere. The good news is that the Primitive classes will generate surface normals for you.
3D scenes
A Java 3D scene graph is a tree with two parts or branches: view and content. The view branch contains all of the gory details of the complex Java 3D viewing model, and it defines the viewpoint. The content branch describes what you see in your scene. It contains all your graphics objects (spheres, boxes, or more complicated geometry objects), as well as the transformations that move them around (i.e., lights, behaviors, group nodes, and fog). For most simple applications, you can use the universe utility classes; the most interesting of which is SimpleUniverse class.
The source code below generates a scene graph with the cylinders that make up the axis and three cones. Then it adds one cone with rotate before translate and another that has translate before rotate. Note: This is an applet code to be shown in the AppletViewer or Internet browser. You can easily convert this program into an independent application.
import java.applet.Applet;import java.awt.BorderLayout;
import java.awt.event.*;
import java.awt.*;
import java.awt.GraphicsConfiguration;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.geometry.*;
import com.sun.j3d.utils.universe.*;
import javax.media.j3d.*;
import javax.vecmath.*;
import com.sun.j3d.utils.behaviors.vp.*;
public class TransformOrder extends Applet {
public static final int X =1;
public static final int Y =2;
public static final int Z =3;
public static final int ROTATE_TOP =4;
public static final int TRANSLATE_TOP =5;
public static final int NO_TRANSFORM =6;
private SimpleUniverse universe ;
private BranchGroup scene;
private Canvas3D canvas;
private BoundingSphere bounds =
new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 1000.0);
private Appearance red = new Appearance();
private Appearance yellow = new Appearance();
private Appearance purple = new Appearance();
Transform3D rotate = new Transform3D();
Transform3D translate = new Transform3D();
public void setupView() {
/** Add some view related things to view branch side
of scene graph */
// add mouse interaction to the ViewingPlatform
OrbitBehavior orbit = new OrbitBehavior(canvas,
OrbitBehavior.REVERSE_ALL|OrbitBehavior.STOP_ZOOM);
orbit.setSchedulingBounds(bounds);
ViewingPlatform viewingPlatform = universe.getViewingPlatform();
// This will move the ViewPlatform back a bit so the
// objects in the scene can be viewed.
viewingPlatform.setNominalViewingTransform();
viewingPlatform.setViewPlatformBehavior(orbit);
}
//construct each branch of the graph, changing the order children added
// since Group node can only have one parent, have to construct
// new translate and rotate group nodes for each branch.
Group rotateOnTop(){
Group root=new Group();
TransformGroup objRotate = new TransformGroup(rotate);
TransformGroup objTranslate = new TransformGroup(translate);
Cone redCone=
new Cone(.3f, 0.7f, Primitive.GENERATE_NORMALS, red);
root.addChild(objRotate);
objRotate.addChild(objTranslate);
objTranslate.addChild(redCone); //tack on red cone
return root;
}
Group translateOnTop(){
Group root=new Group();
TransformGroup objRotate = new TransformGroup(rotate);
TransformGroup objTranslate = new TransformGroup(translate);
Cone yellowCone=
new Cone(.3f, 0.7f, Primitive.GENERATE_NORMALS, yellow);
root.addChild(objTranslate);
objTranslate.addChild(objRotate);
objRotate.addChild(yellowCone); //tack on yellow cone
return root;
}
Group noTransform(){
Cone purpleCone=
new Cone(.3f, 0.7f, Primitive.GENERATE_NORMALS, purple);
return purpleCone;
}
/** Represent an axis using cylinder Primitive. Cylinder is
aligned with Y axis, so we have to rotate it when
creating X and Z axis
*/
public TransformGroup createAxis(int type) {
//appearance and lightingProps are used in
//lighting. Each axis a different color
Appearance appearance = new Appearance();
Material lightingProps = new Material();
Transform3D t = new Transform3D();
switch (type) {
case Z:
t.rotX(Math.toRadians(90.0));
lightingProps.setAmbientColor(1.0f,0.0f,0.0f);
break;
case Y:
// no rotation needed, cylinder aligned with Y already
lightingProps.setAmbientColor(0.0f,1.0f,0.0f);
break;
case X:
t.rotZ(Math.toRadians(90.0));
lightingProps.setAmbientColor(0.0f,0.0f,1.0f);
break;
default:
break;
}
appearance.setMaterial(lightingProps);
TransformGroup objTrans = new TransformGroup(t);
objTrans.addChild( new Cylinder(.03f,2.5f,Primitive.GENERATE_NORMALS,appearance));
return objTrans;
}
/** Create X, Y , and Z axis, and 3 cones. Throws in
some quick lighting to help viewing the scene
*/
public BranchGroup createSceneGraph() {
// Create the root of the branch graph
BranchGroup objRoot = new BranchGroup();
//45 degree rotation around the X axis
rotate.rotX(Math.toRadians(45.0));
//translation up the Y axis
translate.setTranslation(new Vector3f(0.0f,2.0f,1.0f)); //SCD 0.0f));
//Material objects are related to lighting, we'll cover
//that later
Material redProps = new Material();
redProps.setAmbientColor(1.0f,0.0f,0.0f); //red cone
red.setMaterial(redProps);
Material yellowProps = new Material();
yellowProps.setAmbientColor(1.0f,1.0f,0.0f); //yellow cone
yellow.setMaterial(yellowProps);
Material purpleProps = new Material();
purpleProps.setAmbientColor(0.8f,0.0f,0.8f); //purple cone
purple.setMaterial(purpleProps);
// Create a x,y,z axis, and then 3 cone branches
objRoot.addChild(createAxis(X));
objRoot.addChild(createAxis(Y));
objRoot.addChild(createAxis(Z));
objRoot.addChild(noTransform()); //purple cone
objRoot.addChild(rotateOnTop()); //red cone
objRoot.addChild(translateOnTop()); //yellow cone
//throw in some light so we aren't stumbling
//around in the dark
Color3f lightColor = new Color3f(.3f,.3f,.3f);
AmbientLight ambientLight= new AmbientLight(lightColor);
ambientLight.setInfluencingBounds(bounds);
objRoot.addChild(ambientLight);
DirectionalLight directionalLight = new DirectionalLight();
directionalLight.setColor(lightColor);
directionalLight.setInfluencingBounds(bounds);
objRoot.addChild(directionalLight);
return objRoot;
}
public TransformOrder() {
}
public void init() {
BranchGroup scene = createSceneGraph();
setLayout(new BorderLayout());
GraphicsConfiguration config =
SimpleUniverse.getPreferredConfiguration();
canvas = new Canvas3D(config);
add("Center", canvas);
// Create a simple scene and attach it to the virtual universe
universe = new SimpleUniverse(canvas);
setupView();
universe.addBranchGraph(scene);
}
public void destroy() {
universe.removeAllLocales();
}
//
// The following allows TransformOrder to be run as an application
// as well as an applet
//
public static void main(String[] args) {
new MainFrame(new TransformOrder(), 256, 256);
}
}
Additional resources about Java 3D
Peter V. Mikhalenko is a Sun certified professional who works as a business and technical consultant for several top-tier investment banks.
—————————————————————————————
Get Java tips in your inbox
Delivered each Thursday, our free Java Developer newsletter provides insight and hands-on tips you need to unlock the full potential of this programming language. Automatically subscribe today!