Add a Third Dimension to Swing





Add a Third Dimension to Swing

User interfaces have stuck to 2D drawing for many years. Today, Swing and Java3D give you a chance to go one step further and add 3D widgets to your UI.

Have you ever wondered how to add nice 3D components into your Swing applications? Java3D is a free API provided by Sun Microsystems for Linux and Windows, and by Apple for Mac OS X, that lets you create 3D scenes. Although well documented, Java3D seems impossible to use with Swingat least at first glance.

The Problems with Java3D

Imagine you decided to create a new, astonishing application called AmazonPick that would let the user search for books on the Amazon.com store. Your eye-candy user interface would even display the currently selected book as a 3D object; whenever the user selects another book, the 3D object would flip to show the new cover on its opposite side. Figure shows how the application should look.

AmazonPick shows books as full 3D objects


Unfortunately, you won't be able to obtain these results without a little imagination. For instance, take a close look at Figure and notice the gradient background of the window. Displaying such a background is very easy with Swing and the opaque properties of Swing components, as seen in the rather simple class in Figure.

A demo program for 3D components
public BooksDemo() 
{
	super("AmazonPick");

	JButton cover1 = UIHelper.createButton("", "cover1_small_button", true);
	JButton cover2 = UIHelper.createButton("", "cover2_small_button", true);
	JButton cover3 = UIHelper.createButton("", "cover3_small_button", true);
	
	JPanel buttons = new JPanel();
	buttons.add(cover1);
	buttons.add(cover2);
	buttons.add(cover3);
	buttons.setOpaque(false);
	
	setContentPane(new GradientPanel());
	getContentPane().setLayout(new BorderLayout());
	getContentPane().add(buttons, BorderLayout.SOUTH);
	
	pack();
	setResizable(false);
	setDefaultCloseOperation(EXIT_ON_CLOSE);

	UIHelper.centerOnScreen(this); 
}

This code creates three buttons, each containing a picture of a book loaded by the utility class UIHelper, and then puts them in a JPanel. This panel is itself added to the content pane of the window, at the south side. With the help of the setContentPane() method, the default content pane is replaced by a new panelan instance of GradientPanelthat is capable of drawing a nice gradient. To make sure the gradient remains visible in the buttons panel, you need to make the panel transparent. This can be achieved easily by calling setOpaque(false), which will prevent the componentin our case, the panelfrom drawing its background, letting underlying components shine through.

Now, we have to take a slight diversion into AWT and Swing vagaries. With J2SE, you can use two different graphical toolkits to create an application: AWT and Swing. The main difference between those two is that AWT widgets are heavyweight whereas Swing widgets are lightweight. These names come from the very nature of these components. Whatever platform you are running your application on, AWT widgets are drawn using the underlying OS native toolkit. Swing, on the contrary, is completely decoupled from the OS and all the painting is done by Java itself. As a result, AWT widgets are the least common denominator between the various operating systems supported by Java. This also means that advanced features like transparency are pure fantasy with AWT: Swing lets you create transparent components very easily, but AWT does not.

The bad news is that Java3D offers an AWT component only, Canvas3D, to display a 3D scene. So you'll have to mix some Swing and AWT code. Here is how you can add such a component in the Swing UI:

Canvas3D c3d = new Canvas3D(SimpleUniverse.getPreferredConfiguration());
c3d.setSize(CANVAS3D_WIDTH, CANVAS3D_HEIGHT);
getContentPane().add(centerPanel, BorderLayout.CENTER);
createScene();

createScene( ) is responsible for building the 3D scene the Canvas3D will display. Running this code will produce a rather ugly result, as shown in Figure.

A Canvas3D cannot be made transparent


As you can see, you end up with a black background in the Canva3D. Because the only way to get rid of a component's background is to call setOpaque(false)which is defined by JComponent and thus isn't available to AWT componentsyou are stuck with this ugly background. Indeed, as a lightweight component, the canvas cannot be made transparent. Things get even worse when you try to add a menu bar to the application because of the order in which components are painted: lightweight first, heavyweight next. Figure shows an example of what happens when a pop-up menu is drawn by Swing. Because it is a lightweight component, it is drawn before Canvas3D, when it should be drawn after the canvas.

Lightweight components are drawn behind heavyweight components


Thankfully, this new problem (it's all AWT's fault!) was so annoying that the Swing team decided to add a workaround for it. You can simply force all pop-up menus of your applications to be created as heavyweight components instead of lightweight components. Asingle line of code is enough to fix the problem:

JPopupMenu.setDefaultLightWeightPopupEnabled(false); 

If you invoke this method before you create the first JMenu or JPopupMenu, you ensure your menus will be drawn on top of heavyweight components. So, this takes care of one issue, but you still need to deal with the black background problem.

Faking Transparency

Because you cannot change the opacity of the Canvas3D, you are left with only two possible solutions. The first is to get rid of Swing, go back to AWT, and offer a crappy interface to the users. Because this doesn't seem like too great an option, we'll just have to fake transparency.

A Java3D scene is represented as a graph in which every node is an object or a group of objects. Aclose look at the package com.sun.j3d.utils.geometry reveals the existence of the Background class, which you can use to change the background of the 3D scene. For instance, you can create an Alpine scene just by adding a background with a photo of the Alps as its texture. Therefore, to fake transparency, you just have to use the window's content panel as texture for a new Background object that you then add to the scene graph. This is how AmazonPick creates the Java3D scene and adds a special background:

public void createScene()
{
	BranchGroup objRoot = new BranchGroup();
	objRoot.addChild(createBackground());
	// creates the whole scene
}

In Java3D, the scene is an instance of BranchGroup. By adding the Background created by the method createBackground( ) as a child of the scene node, you can set the background of the scene. The background itself is created like this:

protected Background createBackground()
{
	BufferedImage image;
	image = new BufferedImage(c3d.getParent().getWidth(),
                           c3d.getParent().getHeight(),
				   BufferedImage.TYPE_INT_RGB);
	getContentPane().paint(image.getGraphics());

	BufferedImage subImage;
	subImage = new BufferedImage(CANVAS3D_WIDTH,
					 CANVAS3D_HEIGHT,
					 BufferedImage.TYPE_INT_RGB);
	Graphics2D subGraphics = (Graphics2D) subImage.getGraphics();
	subGraphics.drawImage(image, null, -c3d.getX(), -c3d.getY());

	ImageComponent2D backImage;
	backImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGB,
						subImage)
	Background bg = new Background(backImage);
	BoundingSphere bounds = new BoundingSphere();
	bounds.setRadius(100.0);
	bg.setApplicationBounds(bounds);

	return bg; 
} 

The texture is created in two steps. The first is to create a BufferedImage called image on which you paint the content panel of the window. Notice that the picture has the same dimensions as the content panel. Calling the paint() method of the component you want to see through the Java3D scene is less efficient than taking a screen capture with the help of java.awt. Robot; however, it is a lot easier because it works even when the Canvas3D has already been added to the window. This allows, for instance, changing the texture when the window is resized and the gradient changes due to the new dimensions.

Once the content panel has been fully drawn in image, you must clip it to retrieve the exact part covered by the Canvas3D. This is done with another BufferedImage called subImage. This new picture has the same dimensions as the 3D scene. The second step is to draw image on subImage (without forgetting to change the origin of the drawing when you call drawImage()). When the two last parameters of this method are both 0, the image is drawn on the target surface with its top-left corner at the target's top-left corner. With the coordinates c3d.getX() and c3d.getY(), the pixel of image drawn at the top-left corner of subImage is the pixel where the Canvas3D is located on screen. This ensures the code paints the exact part of the content panel that lies behind the 3D scene.

Then, a Background object is created with subImage as a texture. To achieve this, you need to create an ImageComponent2D from the BufferedImageand don't forget to make the pixel formats compatible! Since subImage has its pixels stored as RGB integers, you must do the same for ImageComponent2D.A bounding sphere is finally attached to the background. Java3D uses this sphere to know where the background object needs to be rendered and where it should not be rendered. In this case, the background will appear within a sphere of a radius of 100 units. This value is large enough to prevent any rendering problem. Figure shows the final result, with a "transparent" 3D scene and no bugginess with pop-up menus.

Romain Guy


     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows