Creating realistic graphics in 3D
Michael is a software engineer and researcher for the Department of Defense at the Naval Research Laboratory in Washington, D.C. Michael also founded and functions as CTO of Zizworks Inc. (http://www.zizworks.com/), a web-application and custom software development company. He can be contacted at [email protected]
Go ahead, admit ityou've wasted way too many hours playing the latest first-person-shooter game to hit the shelves. Don't worry, you aren't alone. The realistic environments, quick action, and competitive play make the games irresistible. This is due in part to a wonderful use of three-dimensional (3D) graphics (but mostly due to the love of fragging your friends, which is an article for another time). However, 3D graphics are not just limited to the gaming world. Many industries now rely heavily on 3D graphics for data visualization, building and component design, medical research, virtual tours, and so on. Advertising, especially TV commercials, is also making heavy use of 3D animation and special effects.
Over the past few years computer graphics hardware has made incredible strides with faster CPUs and Graphical Processing Units (GPU) at constantly decreasing prices. On the software front, OpenGL, officially introduced in 1992, has become the standard API for high-speed 3D graphics programming. As a procedural interface developed in C, OpenGL is incredibly powerful, robust, and stable. However, learning OpenGL can be time consuming and, as with all procedural languages, code maintenance and extension can be difficult on large projects. Enter the 3D scenegraph.
A scenegraph provides an object-oriented and logical representation of a 3D scene. Scenegraphs are implemented in many languages and many scenegraphs are simply abstraction layers above the OpenGL rendering library. This abstraction is the foundation of Java3D, a scenegraph API designed and developed by Sun Microsystems for the Java platform. Java3D offers a large 3D API and scenegraph structure to help you write maintainable, scalable 3D applications quickly. In this article, I examine scenegraphs in detail and present an example of a Java3D application.
Java3D is a free library for the Java platform (http://java.sun.com/products/ java-media/3D/). At its most basic level, Java3D provides a scenegraph and 3D rendering context for creating graphics applications. However, that description doesn't give nearly enough information. Some of the top Java3D features include:
- Multithreaded scenegraph rendering and stimulus processing.
- Fog, lighting, level of detail (LOD), and sound support.
- Geometry and texture processing and serialization.
- Low-level API abstraction (supporting both OpenGL and Microsoft's DirectX).
- Vector math operations and full Java Foundation Class (JFC) library support.
As with any Java application, Java3D applications are cross platform to Solaris, Windows, Linux, and Apple OS X. (Java3D is available on OS X in a limited fashion through Apple's developer program. More information can be found at Apple's web site, not Sun's.) Also, Java3D applications are web deployable using Sun's Webstart technology (https://j3d-webstart.dev.java.net/). Backed by OpenGL or DirectX, Java3D boasts impressive rendering speed by allowing these highly optimized libraries to do the rendering work and making use of the native graphics hardware and software drivers. All of these features packed into a free API create a powerful tool.
Understanding the Scenegraph
To understand scenegraphs, it is important to know how a scenegraph differs from the procedural or "pipeline" model of 3D programming. Using a library like OpenGL, the application procedurally defines all of the triangles, textures, and other graphics primitives to draw one after another for each drawing cycle. All of this information is pushed into the graphics pipeline and to the graphics card. Unfortunately, the procedural model can force the application to either send a lot of wasted data into the pipeline, because it is not visible in the current view. Likewise, the application may be forced to perform complex math operations to cull (or remove) unseen information before sending it to the card. The procedural model also requires that the application maintain the state of the scene so that it can be redrawn whenever required.
To solve some of the pipeline rendering limitations and to make developing graphics applications friendlier, the scenegraph paradigm was introduced. A scenegraph is a hierarchical graph of a scene or virtual world. The scenegraph is composed of nodes, which represent mathematical transformations, lighting, shapes, and views. On each rendering cycle, a renderer walks this graph from the top to the bottom, performing many optimizations such as culling nodes that cannot be seen, collapsing nodes that can be combined, and compiling nodes for future rendering. By performing these optimizations, the renderer can limit the number of primitives that get sent to the underlying rendering library and hardware. A scenegraph also provides a logical representation of where objects are in the virtual world and lets you interact with nodes directly in a more object-oriented fashion. On the downside, a scenegraph can introduce larger memory requirements to an application, as well as the time and CPU cycles required to continually traverse the graph.
Java3D is an implementation of the scenegraph paradigm. In Java3D, the scenegraph is encapsulated in a VirtualUniverse, which contains a directed, acyclic graph (DAG) of Nodes, either leaves or groups, such as: BranchGroups, TransformGroups, Shape3Ds, Lights, and ViewingPlatforms. Each node has a specific purpose in the tree and because the graph is a DAG, there is only one possible path to any node in the scene. Nodes may in turn contain NodeComponents to represent items such as appearance and geometry. NodeComponents are not considered part of the graph; therefore, they may be shared between nodes. Figure 1, a simple Java3D scenegraph, contains one branch that defines the viewer of the scene, and one branch from the root that defines the content of the scene. More branches can be added, but this simple scenegraph presents all of the basic concepts. The Java3D renderer is continually rendering the scene, starting at the root node, and traversing down the scene with each TransformGroup applied as it is encountered and each node rendered. Again, the Java3D renderer is able to perform simple view-frustum culling at this stage of the rendering process, far before the data is pushed to the video card.
Getting Your 3D Feet Wet
The first step in creating a Java3D application is to create the virtual universe, which contains the entire scene. Sun provides the utility class SimpleUniverse to make this process straightforward, although a custom universe can be constructed with the VirtualUniverse, View, PhysicalBody, and PhysicalEnvironment classes, but they are beyond the scope of this article. Listing One creates the SimpleUniverse object with the default settings of the utility class. The SimpleUniverse requires a canvas for the renderer to draw into. Similar to the Advanced Windowing Toolkit (AWT) Canvas class, the Java3D Canvas3D class provides a rendering context that can be added to any AWT or Swing container and it behaves as a heavyweight component. The SimpleUniverse class automatically builds the view side of the scenegraph for you with a standard layout that will work for simple applications.
Once the universe has been created, it is time for the fun stuffcreating the scene content. The content needs to attach to a root group node. The basic group node in Java3D is the BranchGroup, which can have any number of children and serves as merely a branching point in the tree. Listing Two is the root group along with another type of group node, a TransformGroupa node containing a 3D transform matrix that applies to the rendering pipeline as the renderer encounters the group in the tree traversal. As with all 3D graphics programming, the placement and orientation of objects in Java3D is determined by the application of matrices to objects. Two transforms are defined in Listing Twoone to scale the content to fit in the canvas, another to rotate any of its children (anything attached to this group) by 35 degrees around the x-axis. In this application, the rotation is simply done to show that the object in the scene truly is 3D. In more complex applications, you make use of many transformations to move the viewer, position objects, simulate animation, and so on. Once the transform group is created, it must be added to the root group as a child. This addition of children is how the graph is built to represent the scene.
At this point there is nothing in the scene for the renderer to actually draw, such as a shape. Java3D defines a Shape3D class as the root for almost all renderable objects in a scene. A Shape3D object contains an Appearance and one or more Geometry components that may be shared between shapes. The Appearance component of a shape defines elements such as color, material, transparency, and drawing attributes. The Geometry component of a shape defines the actual 3D points and lines that are drawn by the underlying graphics library, if the renderer determines that the given shape should be rendered. Once again, to simplify the task of creating shapes, appearances, and geometry, Sun has provided a few utility classes that neatly and efficiently define some common shapes: Box, Cone, Sphere, and ColorCube. To add a ColorCube shape to the scene, add:
ColorCube cube = new ColorCube(.5);
The cube is created with an edge length of 0.5 units and it is added as a child of the rotation transform that was added to the scene previously. Now, add the root group to the universe to assemble the final scene:
Once the scene is assembled and the canvas is displayed, the Java3D renderer immediately begins rendering in a separate thread. The result is the rendered 3D cube, like that in Figure 2.
Behaviors & Interactions
Although an impressive result for such little code, a static 3D scene is not very useful. At some point you are going to require elements such as user interaction, animation, effects, and movement. To accomplish these tasks, Java3D provides a behavior system that works alongside the renderer to provide hooks that allow the application to be notified of events in the scene. Behavior objects are scenegraph nodes like many of the nodes you have already seen and can be added to the scenegraph to perform many functions in response to a large range of stimuli. A small sample of possible stimuli includes:
- A desired number of frames elapsing.
- Collision of 3D objects.
- Mouse and keyboard events.
- A desired amount of time elapsing.
These stimuli are defined by subclasses of the WakeupCondition class, which has many other useful extensions.
Built on top of the behavior system are Interpolator classes that can be used to smoothly move or transform an object in the scene, which is useful for view transitions, morphing, or animation. Listing Three presents modifications to the scenegraph that was constructed above to add another transform group, which will rotate its children about the y-axis. When combined with a RotationInterpolator, the cube appears as a spinning cube in the scene, updated each frame by the interpolator (which, remember, is a Behavior). The effect of the spinning is visible in Figure 3. (A Java3D application demonstrating scenegraph creation using transforms, a predefined shape, and an interpolator behavior is available electronically; see "Resource Center," page 5.)
Tips & Tricks
So far, I have only presented some of the core Java3D concepts. Once you get going with Java3D programming, you may find some tips and tricks useful. These tips may also help you to avoid some of the traps that many new Java3D developers fall into.
Java3D supports complex canvas configurations including multiple canvases and views for the same scene. This means it is possible to render a single scenegraph (a single universe) in many windows simultaneously, either from the same viewpoint in the scene or from many different view points. Combine multiple canvases with a canvas configured for off-screen rendering and it is possible to create dynamic 3D snapshots for web pages. Be aware that the Canvas3D class does have peculiarities. A common problem many developers have with the Canvas3D class is that it is a heavyweight Swing component. This can cause some problems in applications that mix heavyweight and lightweight components, such as JPopupMenus. Be sure to read up on the limitations of mixing these components at http://java.sun.com/products/jfc/tsc/articles/mixing/index.html.
3D applications have a tendency to require a lot of memory due to the geometry definitions and textures required. The -X command-line options of your JVM may let you increase the heap size for the application. In Sun's JVM, the -Xmx and -Xms flags perform wonders. Before undertaking any large Java3D application, be sure to think about the overall design of the application. Because the renderer is continuously rendering in a separate thread, any scenegraph modifications may become immediately visible. If this is not the desired effect (say, you require atomic updates), consider using a Behavior object. Sun, with the help of community developers (http://www.j3d.org/), has composed a document describing items for performance tuning: http://www.j3d.org/tutorials/quick_fix/perf_guide_1_3.html.
Java3D is a powerful library; however, it is not the only 3D API for Java. Numerous lower level APIs are available to provide direct access to the OpenGL rendering library such as JOGL (https://jogl.dev.java.net/) and LWJGL (http://java-game-lib.sourceforge.net/). Also, competitor scenegraph implementations exist, with the most popular and stable being Xith3D (http://xith.org/). Currently, it appears that the Sun Java3D developers are recognizing and encouraging Xith3D as a high-speed, gaming-oriented scenegraph, while Java3D takes a more user-friendly, thread-safe, visualization approach. Luckily, the Xith3D developers have kept the API generally similar to Java3D, so knowledge in one can be easily transferred. Each 3D API has advantages and disadvantages, so review them all before starting a major project.
It seems that Sun is once again behind Java3D, allocating resources toward its development and integration into widely publicized projects, such as its own Project Looking Glass (https://lg3d.dev.java.net/). Many Java3D developers have also released free applications and games that give a glimpse of what is possible with Java3D. Commercial products using Java3D can be found, such as the "Law and Order" game from Legacy Interactive (http://www.lawandordergame.com/). There also appears to be a number of scientific research applications using Java3D that never get public attention; however, discussion commonly occurs on the mailing lists. Figure 4 is a collection of screenshots from a few applications written in Java3D. Sun has recently released Java3D as an open-source project, inviting developers to contribute patches and new frameworks to the API for consideration, which has brought new life and energy to the project.
Java3D is a large API, containing more than 100 core classes and many more utility classes. Learning the entire package is a hefty undertaking. Luckily there are many resources available for more information, such as the Java3D mailing lists and forums, a great tutorial at the Java3D web site, and a few books. Combining Java and Java3D with some of the other powerful Java APIs such as Java Advanced Imaging (JAI), Java2D, and the Java Media Framework (JMF), it is possible to create robust cross-platform applications.
// The canvas needs some information about the graphics environment. This // information could be custom built if desired, but a utility method // exists to make this easier. GraphicsConfiguration gc = SimpleUniverse.getPreferredConfiguration(); // Create the canvas which will serve as the rendering surface. The // canvas is a component like any AWT component, therefore it can // be added to a JFrame to be displayed. Canvas3D canvas = new Canvas3D(gc); // A SimpleUniverse is a utility class that wraps some of the VirtualUniverse // configuration options and sets up a basic universe that is useful for // simple demonstrations. The universe serves as the root of the scenegraph. SimpleUniverse universe = new SimpleUniverse(canvas); // Get the viewing platform from the universe and set a nominal // transform. This will move the viewer slightly back from the // center so you can see the nodes in the scene. universe.getViewingPlatform().setNominalViewingTransform();Back to article
// Root group of scene graph. Everything is created as a child of this group. BranchGroup rootBg = new BranchGroup(); // Create a simple transform to scale scene down so it fits in the view. Transform3D scaleTrans = new Transform3D(); scaleTrans.setScale(0.6); TransformGroup objScale = new TransformGroup(scaleTrans); rootBg.addChild(objScale); // Create a simple transform to rotate around the x // axis to show that the cube really is 3 dimensional. Transform3D rollTrans = new Transform3D(); rollTrans.rotX(Math.toRadians(35)); TransformGroup objRoll = new TransformGroup(rollTrans); objScale.addChild(objRoll);Back to article
// Create a transform to rotate the shape using an interpolator. Once the // transform group is added to the scene, Java3D won't allow modifications // unless you tell it that you want that capability, therefore you set the // ALLOW_TRANSFORM_WRITE. TransformGroup objRotate = new TransformGroup(); objRotate.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); objRoll.addChild(objRotate); // Create an interpolator behavior object that will rotate the cube // by modifying the rotation transform at runtime. Transform3D yAxis = new Transform3D(); Alpha rotationAlpha = new Alpha(-1, Alpha.INCREASING_ENABLE, 0, 0, 8000, 0, 0, 0, 0, 0); // Setup the scheduling bounds of the behavior so it runs indefinitely. Bounds bounds = new BoundingSphere(new Point3d(0, 0, 0), 100.0); // Create the interpolator that will rotate the given transform // around the y axis as the alpha value changes. RotationInterpolator rotator = new RotationInterpolator(rotationAlpha, objRotate, yAxis, 0.0f, (float) Math.PI*2.0f); rotator.setSchedulingBounds(bounds); objRotate.addChild(rotator);Back to article