OK, I've found a bug and an RFE in OrientedShape 3D with constantScaleEnable:
1) The contructor for OrientedShape3D that takes a "scale" doesn't work correctly - it always sets the scale to 1.0. In the example program I'm setting the scale to 10, but it's getting 1 (zoom into the sphere and you can see the label very small). You have to use the view menu to set it to 1 and then back to 10 to get it to work right. Also, it's not clear to me why I need to set the scale to 10 in order to see the label. The documentation says that the default scale is 1.0 meters. It's coming out of Text2D with a size of 20/256 = 0.078 meters, but I have to set the scale to about 5 to make it seem that big. 2) Setting constantScale makes OrientedShape3D useless for labels (which is one of its major purposes). When you move in and out the label appears to move in and out with respect to the sphere it's labelling. This is because the label is remaining at a constant position in Z. So when the user moves in, the sphere appears to move closer, but the label remains at the same distance. This is not the desired effect. Believe it or not, what we need is constant scale in X and Y and *not* constant scale in Z. -Paul
import java.awt.*; import java.awt.event.*; import java.awt.Rectangle; import java.util.AbstractList; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import javax.media.j3d.*; import javax.vecmath.*; import javax.swing.*; import com.sun.j3d.utils.geometry.Sphere; import com.sun.j3d.utils.universe.PlatformGeometry; import com.sun.j3d.utils.universe.SimpleUniverse; import com.sun.j3d.utils.universe.ViewingPlatform; import com.sun.j3d.utils.image.TextureLoader; import com.sun.j3d.utils.behaviors.vp.OrbitBehavior; import com.sun.j3d.utils.behaviors.keyboard.KeyNavigatorBehavior; import com.sun.j3d.utils.applet.JMainFrame; import org.j3d.geom.RasterTextLabel; import org.j3d.geom.*; public final class OS3DBugs extends JApplet { private static final Geometry sphereGeometry; private KeyNavigatorBehavior keyBehavior; private TransformGroup vptg; private static final float MOVE_BACK = 2.0f; private BranchGroup objRoot = null; private static final Color3f BLACK = new Color3f(0f, 0f, 0f); private static final Color3f WHITE = new Color3f(1f, 1f, 1f); private static final Color3f GREEN = new Color3f(0.5f, 1f, 0.5f); private SimpleUniverse u; private PlatformGeometry pg; private BoundingSphere bounds; private static final Point3d D_ORIGIN = new Point3d(0, 0, 0); private static final Point3f F_ORIGIN = new Point3f(0, 0, 0); private Canvas3D c3d; private boolean constantScale = true; private boolean scaleOne = false; private OrientedShape3D os3d; private boolean parallel = false; private static final float SPHERE_RADIUS = 0.2f; static { final int SPHERE_TESSELATION = 20; Sphere s = new Sphere(1.0f, Sphere.GENERATE_NORMALS, SPHERE_TESSELATION, null); sphereGeometry = s.getShape().getGeometry(); } // End of static initializer /** * Set up keyboard navigation. */ private void addKeyBehavior(BranchGroup b) { // Set up keyboard navigation keyBehavior = new KeyNavigatorBehavior( u.getViewingPlatform().getViewPlatformTransform()); keyBehavior.setSchedulingBounds(bounds); // Add behavior to scenegraph b.addChild(keyBehavior); } // End of addKeyBehavior /** * Initialize and return the Canvas3D we'll be using for rendering. */ public void init() { bounds = new BoundingSphere(D_ORIGIN, Double.POSITIVE_INFINITY); // Use utility to get the right graphics destination GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration(); // Create a canvas on the device c3d = new Canvas3D(config); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); contentPane.add(c3d, "Center"); JPopupMenu.setDefaultLightWeightPopupEnabled(false); JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); JMenu viewMenu = new JMenu("View"); menuBar.add(viewMenu); JMenuItem constantScaleItem = new JMenuItem("not constantScale"); viewMenu.add(constantScaleItem); constantScaleItem.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { JMenuItem constantScaleItem = (JMenuItem)e.getSource(); constantScale = !constantScale; os3d.setConstantScaleEnable(constantScale); if (constantScale) { constantScaleItem.setText("not constantScale"); } else { constantScaleItem.setText("constantScale"); } } } ); JMenuItem scaleOneItem = new JMenuItem("scale = 1"); viewMenu.add(scaleOneItem); scaleOneItem.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { JMenuItem scaleOneItem = (JMenuItem)e.getSource(); scaleOne = !scaleOne; if (scaleOne) { scaleOneItem.setText("scale = 10"); os3d.setScale(1.0); } else { scaleOneItem.setText("scale = 1"); os3d.setScale(10.0); } } } ); JMenuItem projectionItem = new JMenuItem("Parallel Projection"); viewMenu.add(projectionItem); projectionItem.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { JMenuItem projectionItem = (JMenuItem)e.getSource(); parallel = !parallel; if (parallel) { c3d.getView().setProjectionPolicy(View.PARALLEL_PROJECTION); } else { c3d.getView().setProjectionPolicy(View.PERSPECTIVE_PROJECTION); } } } ); // Use utility to create default universe u = new SimpleUniverse(c3d); // Set the viewpoint back from the origin and point toward the origin ViewingPlatform vp = u.getViewingPlatform(); vp.setNominalViewingTransform(); vptg = vp.getViewPlatformTransform(); Transform3D vpt = new Transform3D(); vptg.getTransform(vpt); Transform3D translate = new Transform3D(); translate.setTranslation(new Vector3f(0f,0f,MOVE_BACK)); vpt.mul(translate); vptg.setTransform(vpt); // Allow mouse manipulation OrbitBehavior orbit = new OrbitBehavior(c3d, OrbitBehavior.REVERSE_ALL); // Behavior is only enabled within its bounds bounds = new BoundingSphere(D_ORIGIN, 100.0); orbit.setSchedulingBounds(bounds); // Attach behavior to ViewingPlatform vp.setViewPlatformBehavior(orbit); // Will add lights here so they are "headlights" // that move with the viewer pg = new PlatformGeometry(); // Set up the ambient light Color3f ambientColor = new Color3f(0.1f, 0.1f, 0.1f); AmbientLight ambientLightNode = new AmbientLight(ambientColor); ambientLightNode.setInfluencingBounds(bounds); pg.addChild(ambientLightNode); // Set up the directional lights Color3f light1Color = new Color3f(1.0f, 1.0f, 0.9f); Vector3f light1Direction = new Vector3f(1.0f, 1.0f, 1.0f); Color3f light2Color = new Color3f(1.0f, 1.0f, 1.0f); Vector3f light2Direction = new Vector3f(-1.0f, -1.0f, -1.0f); DirectionalLight light1 = new DirectionalLight(light1Color, light1Direction); light1.setInfluencingBounds(bounds); pg.addChild(light1); DirectionalLight light2 = new DirectionalLight(light2Color, light2Direction); light2.setInfluencingBounds(bounds); pg.addChild(light2); vp.setPlatformGeometry(pg); objRoot = new BranchGroup(); objRoot.setCapability(objRoot.ALLOW_DETACH); objRoot.setCapability(objRoot.ALLOW_CHILDREN_EXTEND); objRoot.setCapability(objRoot.ALLOW_CHILDREN_WRITE); addKeyBehavior(objRoot); objRoot.compile(); u.addBranchGraph(objRoot); addNode(); } // End of init /** * Add a node to the graph. */ public void addNode() { // Each node is connected by a BranchGroup (to allow it to // be removed) BranchGroup root = new BranchGroup(); // Transform Group to hold position transform TransformGroup posTg = new TransformGroup(); Transform3D pos = new Transform3D(); Vector3f translate = new Vector3f(0.2f,0f,0f); pos.setTranslation(translate); posTg.setTransform(pos); root.addChild(posTg); // TransformGroup to hold scale transform TransformGroup scaleTg = new TransformGroup(); Transform3D scale = new Transform3D(); scale.setScale(SPHERE_RADIUS); scaleTg.setTransform(scale); posTg.addChild(scaleTg); // Set appearance of node RenderingAttributes ra = new RenderingAttributes(); Material mat = new Material(BLACK, BLACK, GREEN, WHITE, 128.0f); Appearance a = new Appearance(); a.setMaterial(mat); a.setRenderingAttributes(ra); Shape3D shape = new Shape3D(sphereGeometry, a); scaleTg.addChild(shape); BranchGroup bg = new BranchGroup(); MyText2D t2d = new MyText2D("OrientedShape3D constantScaleEnable", WHITE, null, 20, 0); QuadArray qa = (QuadArray)t2d.getGeometry(); float[] ar = new float[12]; qa.getCoordinates(0, ar); float width = t2d.getImageWidth(); float height = t2d.getImageHeight(); for (int i = 0 ; i < 4 ; i++) { // Center ar[0 + i * 3] -= width / 2; ar[1 + i * 3] -= height / 2; // Move forward in front of sphere ar[2 + i * 3] = SPHERE_RADIUS; } qa.setCoordinates(0, ar); Appearance labelApp = t2d.getAppearance(); os3d = new OrientedShape3D(qa, labelApp, OrientedShape3D.ROTATE_ABOUT_POINT, F_ORIGIN, true, 10.0); os3d.setCapability(OrientedShape3D.ALLOW_SCALE_WRITE); posTg.addChild(os3d); root.compile(); objRoot.addChild(root); } // End of addNode public static void main(String[] args) { new JMainFrame(new OS3DBugs(args), 500, 500); } public OS3DBugs(String[] args) { } public OS3DBugs() { } } // End of file RenderEngine.java
import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.awt.image.DataBufferInt; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Toolkit; import java.util.Hashtable; import javax.media.j3d.*; import javax.vecmath.Color3f; /** * A Text2D object is a representation of a string as a texture mapped * rectangle. The texture for the rectangle shows the string as rendered in * the specified color with a transparent background. The appearance of the * characters is specified using the font indicated by the font name, size * and style (see java.awt.Font). The approximate height of the rendered * string will be the font size times the rectangle scale factor, which has a * default value of 1/256. For example, a 12 point font will produce * characters that are about 12/256 = 0.047 meters tall. The lower left * corner of the rectangle is located at (0,0,0) with the height * extending along the positive y-axis and the width extending along the * positive x-axis.<p> * Java 3D texture maps must be a power of two. Therefore, * the texture mapped onto the quad by Text2D (and the quad) * may be much bigger than they need to be. The only way to * get the actual size of the text is withing Text2D, so we'd * have to import the utility into our code. */ public class MyText2D extends Shape3D { // This table caches FontMetrics objects to avoid the huge cost // of re-retrieving metrics for a font we've already seen. private static Hashtable metricsTable = new Hashtable(); float rectangleScaleFactor = 1f/256; Color3f color = new Color3f(); String fontName; int fontSize, fontStyle; String text; float imageWidth, imageHeight; /** * Creates a Shape3D object which holds a * rectangle that is texture-mapped with an image that has * the specified text written with the specified font * parameters. * * @param text The string to be written into the texture map. * @param color The color of the text string. * @param fontName The name of the Java font to be used for * the text string. * @param fontSize The size of the Java font to be used. * @param fontStyle The style of the Java font to be used. */ public MyText2D(String text, Color3f color, String fontName, int fontSize, int fontStyle) { this.color.set(color); this.fontName = fontName; this.fontSize = fontSize; this.fontStyle = fontStyle; this.text = text; updateText2D(text, color, fontName, fontSize, fontStyle); } /* * Changes text of this Text2D to 'text'. All other * parameters (color, fontName, fontSize, fontStyle * remain the same. * @param text The string to be set. */ public void setString(String text){ this.text = text; ImageComponent imageComponent = setupImage(text, color, fontName, fontSize, fontStyle); getAppearance().getTexture().setImage(0, imageComponent); } private void updateText2D(String text, Color3f color, String fontName, int fontSize, int fontStyle) { ImageComponent imageComponent = setupImage(text, color, fontName, fontSize, fontStyle); Texture2D t2d = setupTexture(imageComponent); QuadArray rect = setupGeometry(imageComponent.getWidth(), imageComponent.getHeight()); setGeometry(rect); Appearance appearance = setupAppearance(t2d); setAppearance(appearance); } /** * Sets the scale factor used in converting the image width/height * to width/height values in 3D. * * @param newScaleFactor The new scale factor. */ public void setRectangleScaleFactor(float newScaleFactor) { rectangleScaleFactor = newScaleFactor; } /** * Gets the current scale factor being used in converting the image * width/height to width/height values in 3D. * * @return The current scale factor. */ public float getRectangleScaleFactor() { return rectangleScaleFactor; } /** * Create the ImageComponent and Texture object. */ private Texture2D setupTexture(ImageComponent imageComponent) { Texture2D t2d = new Texture2D(Texture2D.BASE_LEVEL, Texture.RGBA, imageComponent.getWidth(), imageComponent.getHeight()); t2d.setMinFilter(t2d.BASE_LEVEL_LINEAR); t2d.setMagFilter(t2d.BASE_LEVEL_LINEAR); t2d.setImage(0, imageComponent); t2d.setEnable(true); return t2d; } /** * Creates a ImageComponent2D of the correct dimensions for the * given font attributes. Draw the given text into the image in * the given color. The background of the image is transparent * (alpha = 0). */ private ImageComponent setupImage(String text, Color3f color, String fontName, int fontSize, int fontStyle) { Toolkit toolkit = Toolkit.getDefaultToolkit(); Font font = new java.awt.Font(fontName, fontStyle, fontSize); FontMetrics metrics; if ((metrics = (FontMetrics)metricsTable.get(font)) == null) { metrics = toolkit.getFontMetrics(font); // deprecated metricsTable.put(font, metrics); } int width = metrics.stringWidth(text); int descent = metrics.getMaxDescent(); int ascent = metrics.getMaxAscent(); int leading = metrics.getLeading(); int height = descent + ascent; imageWidth = (float)width * rectangleScaleFactor; imageHeight = (float)height * rectangleScaleFactor; // Need to make width/height powers of 2 because of Java3d texture // size restrictions int pow = 1; for (int i = 1; i < 32; ++i) { pow *= 2; if (width <= pow) break; } width = Math.max (width, pow); pow = 1; for (int i = 1; i < 32; ++i) { pow *= 2; if (height <= pow) break; } height = Math.max (height, pow); // For now, jdk 1.2 only handles ARGB format, not the RGBA we want BufferedImage bImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics offscreenGraphics = bImage.createGraphics(); // First, erase the background to the text panel - set alpha to 0 Color myFill = new Color(0f, 0f, 0f, 0f); offscreenGraphics.setColor(myFill); offscreenGraphics.fillRect(0, 0, width, height); // Next, set desired text properties (font, color) and draw String offscreenGraphics.setFont(font); Color myTextColor = new Color(color.x, color.y, color.z, 1f); offscreenGraphics.setColor(myTextColor); offscreenGraphics.drawString(text, 0, height - descent); ImageComponent imageComponent = new ImageComponent2D(ImageComponent.FORMAT_RGBA, bImage); return imageComponent; } /** * Creates a rectangle of the given width and height and sets up * texture coordinates to map the text image onto the whole surface * of the rectangle (the rectangle is the same size as the text image) */ private QuadArray setupGeometry(int width, int height) { float zPosition = 0f; float rectWidth = (float)width * rectangleScaleFactor; float rectHeight = (float)height * rectangleScaleFactor; float[] verts1 = { rectWidth, 0f, zPosition, rectWidth, rectHeight, zPosition, 0f, rectHeight, zPosition, 0f, 0f, zPosition }; float[] texCoords = { 0f, -1f, 0f, 0f, (-1f), 0f, (-1f), -1f }; QuadArray rect = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); rect.setCoordinates(0, verts1); rect.setTextureCoordinates(0, 0, texCoords); return rect; } /** * Creates Appearance for this Shape3D. This sets transparency * for the object (we want the text to be "floating" in space, * so only the text itself should be non-transparent. Also, the * appearance disables lighting for the object; the text will * simply be colored, not lit. */ private Appearance setupAppearance(Texture2D t2d) { TransparencyAttributes transp = new TransparencyAttributes(); transp.setTransparencyMode(TransparencyAttributes.BLENDED); transp.setTransparency(0f); Appearance appearance = new Appearance(); appearance.setTransparencyAttributes(transp); appearance.setTexture(t2d); Material m = new Material(); m.setLightingEnable(false); appearance.setMaterial(m); return appearance; } /** * Returns the text string * */ public String getString() { return text; } /** * Returns the color of the text * */ public Color3f getColor() { return color; } /** * Returns the font * */ public String getFontName() { return fontName; } /** * Returns the font size * */ public int getFontSize() { return fontSize; } /** * Returns the font style * */ public int getFontStyle() { return fontStyle; } public float getImageWidth() { return imageWidth; } public float getImageHeight() { return imageHeight; } }