Billboard

Hello,



I have lots of texts placed inside a billboard, however, when I rotate camera, the texts rotates with the scene. I want the texts to be looking at camera. lookAt() method does not seem to take effect.



Appreciate your help! Thanks.







Hi,

I used Node instead of BillboardNode, and set the node and its children texts to lookAt the Camera.

This made the text to stay at the same place. However, only the first character of each texts are OK, and others are rotating. This is weird.

Did anybody have the same problem?

I finally got to the point where the texts appear stuck to the screen as I want them to be

when camera is rotating in vertical direction.



When the camera is rotating horizontally, the texts appear rotating horizontally away from the camera.



I applied lookAt() only to the parent node of the texts.



Is there reason for this occurance? I am confused.


Is this a known bug??

I am wondering if this is a bug since I have been struggling to put a stationary text3D for quite a while now.

Maybe some test code, or a pic would help.

Are you using something like a shared node for the text? Are you (re)using the same transformation (quaternion) for all your text nodes?


Hi, I got it working for my purposes, finally: I just found out I cannot do non-uniform scaling on the texts.

In the code example, the word, "here" is rotating in weird way. Please give some explanation as to why or fix it if it is a problem. Thanks.

The following message is the code.

/*

  • Copyright © 2003-2006 jMonkeyEngine All rights reserved. Redistribution and
  • use in source and binary forms, with or without modification, are permitted
  • provided that the following conditions are met: * Redistributions of source
  • code must retain the above copyright notice, this list of conditions and the
  • following disclaimer. * Redistributions in binary form must reproduce the
  • above copyright notice, this list of conditions and the following disclaimer
  • in the documentation and/or other materials provided with the distribution. *
  • Neither the name of 'jMonkeyEngine' nor the names of its contributors may be
  • used to endorse or promote products derived from this software without
  • specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT
  • HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  • INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  • FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  • COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  • INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  • LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  • OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
  • EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    */



    import java.awt.BorderLayout;

    import java.awt.Canvas;

    import java.awt.Color;

    import java.awt.Dimension;

    import java.awt.Font;

    import java.awt.Point;

    import java.awt.event.ComponentAdapter;

    import java.awt.event.ComponentEvent;

    import java.awt.event.InputEvent;

    import java.awt.event.MouseAdapter;

    import java.awt.event.MouseEvent;

    import java.awt.event.MouseMotionListener;

    import java.awt.event.MouseWheelEvent;

    import java.awt.event.MouseWheelListener;

    import java.util.concurrent.Callable;



    import javax.swing.JFrame;

    import javax.swing.JPanel;



    import com.jme.math.FastMath;

    import com.jme.math.Quaternion;

    import com.jme.math.Vector2f;

    import com.jme.math.Vector3f;

    import com.jme.renderer.Camera;

    import com.jme.renderer.ColorRGBA;

    import com.jme.scene.Geometry;

    import com.jme.scene.Node;

    import com.jme.scene.SceneElement;

    import com.jme.scene.Text;

    import com.jme.scene.state.RenderState;

    import com.jme.scene.state.TextureState;

    import com.jme.scene.state.ZBufferState;

    import com.jme.system.DisplaySystem;

    import com.jme.util.GameTaskQueue;

    import com.jme.util.GameTaskQueueManager;

    import com.jmex.awt.JMECanvas;

    import com.jmex.awt.SimpleCanvasImpl;

    import com.jmex.font3d.Font3D;

    import com.jmex.font3d.Text3D;

    import com.jmex.font3d.TextFactory;



    public class Test extends JFrame {

      int width = 640, height = 480;



      MyImplementor impl;

      private CameraHandler camhand;

      private Canvas glCanvas;

      private Node root;

      private Geometry grid;

      Node textNode = new Node("textNode");

     

      public static void main(String[] args) {

          new Test();

      }



      public Test() {

          try {

            init();

            setLocationRelativeTo(null);

            setVisible(true);

            new Thread() {

                {

                  setDaemon(true);

                }



                public void run() {

                  try {

                      while (true) {

                        if (isVisible())

                            glCanvas.repaint();

                        Thread.sleep(2);

                      }

                  } catch (InterruptedException e) {

                  }

                }

            }.start();



          } catch (Exception ex) {

          }

      }



      private void init() throws Exception {

          setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

          setFont(new Font("Arial", 0, 12));



          JPanel canvasPanel = new JPanel();

          canvasPanel.setLayout(new BorderLayout());

          canvasPanel.add(getGlCanvas(), BorderLayout.CENTER);



          getContentPane().add(canvasPanel, BorderLayout.CENTER);



          setSize(new Dimension(1024, 768));

      }



      private Color makeColor(ColorRGBA rgba, boolean useAlpha) {

          return new Color(rgba.r, rgba.g, rgba.b, (useAlpha ? rgba.a : 1f));

      }



      private ColorRGBA makeColorRGBA(Color color) {

          return new ColorRGBA(color.getRed() / 255f, color.getGreen() / 255f,

                color.getBlue() / 255f, color.getAlpha() / 255f);

      }



      protected Canvas getGlCanvas() {

          if (glCanvas == null) {



            glCanvas = DisplaySystem.getDisplaySystem().createCanvas(width, height);

            glCanvas.setMinimumSize(new Dimension(100, 100));



            glCanvas.addComponentListener(new ComponentAdapter() {

                public void componentResized(ComponentEvent ce) {

                  doResize();

                }

            });



            camhand = new CameraHandler();



            glCanvas.addMouseWheelListener(camhand);

            glCanvas.addMouseListener(camhand);

            glCanvas.addMouseMotionListener(camhand);



            // Important! Here is where we add the guts to the canvas:

                impl = new MyImplementor(width, height);



            ((JMECanvas) glCanvas).setImplementor(impl);



            Callable<?> exe = new Callable() {

                public Object call() {

                  forceUpdateToSize();

                  return null;

                }

            };

            GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER).enqueue(exe);

          }

          return glCanvas;

      }



      public void forceUpdateToSize() {

          // force a resize to ensure proper canvas size.

          glCanvas.setSize(glCanvas.getWidth(), glCanvas.getHeight() + 1);

          glCanvas.setSize(glCanvas.getWidth(), glCanvas.getHeight() - 1);

      }



      class CameraHandler extends MouseAdapter implements MouseMotionListener,

      MouseWheelListener {

          Point last = new Point(0, 0);

          Vector3f focus = new Vector3f();

          private Vector3f vector = new Vector3f();

          private Quaternion rot = new Quaternion();



          public void mouseDragged(final MouseEvent arg0) {

            Callable<?> exe = new Callable() {

                public Object call() {

                  int difX = last.x - arg0.getX();

                  int difY = last.y - arg0.getY();

                  int mult = arg0.isShiftDown() ? 10 : 1;

                  last.x = arg0.getX();

                  last.y = arg0.getY();



                  int mods = arg0.getModifiers();

                  if ((mods & InputEvent.BUTTON1_MASK) != 0) {

                      rotateCamera(Vector3f.UNIT_Y, difX * 0.0025f);

                      rotateCamera(impl.getRenderer().getCamera().getLeft(),

                            -difY * 0.0025f);

                  }

                  if ((mods & InputEvent.BUTTON2_MASK) != 0 && difY != 0) {

                      zoomCamera(difY * mult);

                  }

                  if ((mods & InputEvent.BUTTON3_MASK) != 0) {

                      panCamera(-difX, -difY);

                  }

                  return null;

                }

            };

            GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER)

            .enqueue(exe);

          }



          public void mouseMoved(MouseEvent arg0) {

          }



          public void mousePressed(MouseEvent arg0) {

            last.x = arg0.getX();

            last.y = arg0.getY();

          }



          public void mouseWheelMoved(final MouseWheelEvent arg0) {

            Callable<?> exe = new Callable() {

                public Object call() {

                  zoomCamera(arg0.getWheelRotation()

                        * (arg0.isShiftDown() ? -100 : -20));

                  return null;

                }

            };

            GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER)

            .enqueue(exe);

          }



          public void recenterCamera() {

            Callable<?> exe = new Callable() {

                public Object call() {

                  Camera cam = impl.getRenderer().getCamera();

                  Vector3f.ZERO.subtract(focus, vector);

                  cam.getLocation().addLocal(vector);

                  focus.addLocal(vector);

                  cam.onFrameChange();

                  return null;

                }

            };

            GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER)

            .enqueue(exe);

          }



          private void rotateCamera(Vector3f axis, float amount) {

            Camera cam = impl.getRenderer().getCamera();

            if (axis.equals(cam.getLeft())) {

                float elevation = -FastMath.asin(cam.getDirection().y);

                amount = Math.min(Math.max(elevation + amount,

                      -FastMath.HALF_PI), FastMath.HALF_PI)

                      - elevation;

              }

            rot.fromAngleAxis(amount, axis);

            cam.getLocation().subtract(focus, vector);

            rot.mult(vector, vector);

            focus.add(vector, cam.getLocation());

            rot.mult(cam.getLeft(), cam.getLeft());

            rot.mult(cam.getUp(), cam.getUp());

            rot.mult(cam.getDirection(), cam.getDirection());

            cam.normalize();

            cam.onFrameChange();

          }



          private void panCamera(float left, float up) {

            Camera cam = impl.getRenderer().getCamera();

            cam.getLeft().mult(left, vector);

            vector.scaleAdd(up, cam.getUp(), vector);

            cam.getLocation().addLocal(vector);

            focus.addLocal(vector);

            cam.onFrameChange();

          }



          private void zoomCamera(float amount) {

            Camera cam = impl.getRenderer().getCamera();

            float dist = cam.getLocation().distance(focus);

            amount = dist - Math.max(0f, dist - amount);

            cam.getLocation().scaleAdd(amount, cam.getDirection(),

                  cam.getLocation());

            cam.onFrameChange();

          }

      }



      protected void doResize() {

          if (impl != null) {

            impl.resizeCanvas(glCanvas.getWidth(), glCanvas.getHeight());

            if (impl.getCamera() != null) {

                Callable<?> exe = new Callable() {

                  public Object call() {

                      impl.getCamera().setFrustum(-10, 100, -10f, 10f, -10f, 10f);

                      return null;

                  }

                };

                GameTaskQueueManager.getManager()

                .getQueue(GameTaskQueue.RENDER).enqueue(exe);

            }

          }

      }



      class MyImplementor extends SimpleCanvasImpl {



          private static final int GRID_LINES = 51;

          private static final float GRID_SPACING = 100f;



          /

          * The root node of our text.

          */

          protected Node fpsNode;



          /


          * Displays all the lovely information at the bottom.

          */

          protected Text fps;



          /

          * This is used to recieve getStatistics calls.

          */

          protected StringBuffer tempBuffer = new StringBuffer();



          /


            * This is used to display print text.

            /

          protected StringBuffer updateBuffer = new StringBuffer(30);



          public MyImplementor(int width, int height) {

              super(width, height);

          }



          public void simpleSetup() {

              renderer.setBackgroundColor(makeColorRGBA(new Color(0, 0, 0)));

              cam.setParallelProjection(true);

              cam.setFrustum(-10, 100, -10f, 10f, -10f, 10f);



              Vector3f loc = new Vector3f(0, 0, 20);

              Vector3f left = new Vector3f(-1, 0, 0);

              Vector3f up = new Vector3f(0, 1, 0);

              Vector3f look = new Vector3f(0, 0, -1);

              cam.setFrame(loc, left, up, look);



              root = rootNode;



              // Then our font Text object.

              /
    * This is what will actually have the text at the bottom. /

              fps = Text.createDefaultTextLabel("FPS label");

              fps.setCullMode(SceneElement.CULL_NEVER);

              fps.setTextureCombineMode(TextureState.REPLACE);



              // Finally, a stand alone node (not attached to root on purpose)

              fpsNode = new Node("FPS node");

              fpsNode.setRenderState(fps.getRenderState(RenderState.RS_ALPHA));

              fpsNode.setRenderState(fps.getRenderState(RenderState.RS_TEXTURE));

              fpsNode.attachChild(fps);

              fpsNode.setCullMode(SceneElement.CULL_NEVER);



              renderer.enableStatistics(true);



              root.attachChild(grid = createGrid());

              grid.updateRenderState();



              ZBufferState zbuf = renderer.createZBufferState();

              zbuf.setWritable(false);

              zbuf.setEnabled(true);

              zbuf.setFunction(ZBufferState.CF_LEQUAL);



              fpsNode.updateGeometricState(0, true);

              fpsNode.updateRenderState();



              Font3D font = new Font3D(new Font("Arial", 1, Font.PLAIN), 1, true, true, true);

              Text3D text = font.createText("Hi", 10f, 0);

              text.setLocalScale(0.5f);

              Text3D text2 = font.createText("there", 10f, 0);

              text2.setLocalScale(0.5f);

              text2.setLocalTranslation(new Vector3f(0, -0.5f, 0));



              Text3D text3 = font.createText("Here", 10f, 0);

              text3.setLocalScale(new Vector3f(0.5f, 0.5f, 0.01f));

              text3.setLocalTranslation(new Vector3f(0, -1.0f, 0));

             

              Text3D text4 = font.createText("is", 10f, 0);

              text4.setLocalScale(0.5f);

              text4.setLocalTranslation(new Vector3f(0, -1.5f, 0));



              textNode.attachChild(text);         

              textNode.attachChild(text2);

              textNode.attachChild(text3);

              textNode.attachChild(text4);

          }



          public void simpleUpdate() {

              updateBuffer.setLength(0);

              updateBuffer.append("FPS: ").append((int) timer.getFrameRate())

              .append(" - ");

              updateBuffer.append(renderer.getStatistics(tempBuffer));

              /
    * Send the fps to our fps bar at the bottom. */

              fps.print(updateBuffer);

          }



          @Override

          public void simpleRender() {

              fpsNode.draw(renderer);

              renderer.clearStatistics();

             

              textNode.setLocalTranslation(cam.getWorldCoordinates(new Vector2f(glCanvas.getWidth() - 100, glCanvas.getHeight() - 100), 0.99f));

              textNode.updateGeometricState(0, true);         



              textNode.lookAt(cam.getLocation(), cam.getUp());

              textNode.updateGeometricState(0, true);

             

              renderer.draw(textNode);

          }



          private Geometry createGrid() {

              Vector3f[] vertices = new Vector3f[GRID_LINES * 2 * 2];

              float edge = GRID_LINES / 2 * GRID_SPACING;

              for (int ii = 0, idx = 0; ii < GRID_LINES; ii++) {

                float coord = (ii - GRID_LINES / 2) * GRID_SPACING;

                vertices[idx++] = new Vector3f(-edge, 0f, coord);

                vertices[idx++] = new Vector3f(+edge, 0f, coord);

                vertices[idx++] = new Vector3f(coord, 0f, -edge);

                vertices[idx++] = new Vector3f(coord, 0f, +edge);

              }

              Geometry grid = new com.jme.scene.Line("grid", vertices, null,

                    null, null);

              grid.getBatch(0).getDefaultColor().set(ColorRGBA.darkGray.clone());

              return grid;

          }

      }

    }

Your test looks like your are just trying to add HUD text.



If so, why not just use orthographic mode for it?



Also, you do know you are in parallel projection mode right?


Well,

I do not know how to properly use ortho mode. ??

Yeah, I am using parrallel projection intentionally.

Thanks.

Spatial.setRenderQueueMode(Renderer.QUEUE_ORTHO);



also, check out some of the examples, specifically Test3DText.


I tried that and the texts did not show up.

Maybe they don't show up because they are outside of the viewable area… in Ortho mode, the frustum is not more than an parallelepiped, so the view does not broaden with distance from camera. This might feel counter intuitive.