GText

i know, true type text has already beed discussed n+1 times, but none of the given solutions fit my needs.

the text3d class in jme takes quite a lot of memory (problem on old pc's, like the ones i used for testing) and gives a lot of warnings/errors… i don't like them :slight_smile: furthermore, there are a few problems if set in ortho (even if z is scaled to 0.01f)… at least i ran into them…



the normal text class in jme is monospaced, which is ugly

the other text classes found in the forum i couldn't really manage to implement, had a few bugs, or i didn't understand. +many use non power of two textures, and on really stupid videocards look ugly as hell.



as i am developing my own (and soon to be given to jme) gui system for jme2, i wrote my own class. it's as fast i could get it to be, extends a node and uses power of two textures, while still managing text kerneling. and it's pretty.



here are the classes, first the font class (aka lots of textures):

package gen.gui;

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.LinkedList;

import com.jme.image.Texture;
import com.jme.renderer.Renderer;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

/**
 *
 * @author Victor Porof, blue_veek@yahoo.com
 */
public class GFont {

   private int type = BufferedImage.TYPE_INT_ARGB;
   private Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer();
   private BufferedImage tempImage;
   private LinkedList<TextureState> charList;
   private int totalCharSet = 256;
   private int fontWidth = 1;
   private int fontHeight = 1;
   private int[] tWidths;
   private int tHeights;
   private int tAscent;
   private int tDescent;

   public GFont(Font font) {
      tempImage = new BufferedImage(fontWidth, fontHeight, type);
      Graphics2D g = (Graphics2D) tempImage.getGraphics();
      g.setFont(font);

      this.tAscent = g.getFontMetrics().getAscent();
      this.tDescent = g.getFontMetrics().getDescent();

      this.charList = new LinkedList<TextureState>();

      this.tWidths = new int[totalCharSet];
      this.tHeights = tAscent + tDescent;

      for (int i = 0; i < totalCharSet; i++) {
         this.fontWidth = g.getFontMetrics().charWidth((char) i);
         this.fontHeight = g.getFontMetrics().getHeight();
         if (fontWidth < 1) {
            fontWidth = 1;
         }
         tWidths[i] = fontWidth;

         int size = font.getSize();
         float posX = font.getSize() / 2f - fontWidth / 2f;
         float posY = tAscent - tDescent;
         BufferedImage bImage = new BufferedImage(size, size, type);
         Graphics2D gt = (Graphics2D) bImage.getGraphics();
         gt.setFont(font);
         gt.drawString(String.valueOf((char) i), posX, posY);

         TextureState cTextureState = renderer.createTextureState();
         Texture cTexure = TextureManager.loadTexture(bImage,
               Texture.MinificationFilter.Trilinear,
               Texture.MagnificationFilter.Bilinear, true);
         cTextureState.setTexture(cTexure);

         charList.add(cTextureState);
      }
   }

   public TextureState getChar(int charCode) {
      return charList.get(charCode);
   }

   public float getTextAscent() {
      return tAscent;
   }

   public float getTextDescent() {
      return tDescent;
   }

   public int[] getMetricsWidths() {
      return tWidths;
   }

   public int getMetricsHeights() {
      return tHeights;
   }
}



the text class:
ps: the width variable stores the width of the current text

package gen.gui;

import java.util.LinkedList;

import com.jme.renderer.ColorRGBA;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.BlendState.DestinationFunction;
import com.jme.scene.state.BlendState.SourceFunction;

/**
 *
 * @author Victor Porof, blue_veek@yahoo.com
 */
public class GText extends GBaseElement {

   /**
    *
    */
   private static final long serialVersionUID = 1L;
   public GFont gFont;
   private String text = "";
   private ColorRGBA fill;
   private LinkedList<Quad> charQuads = new LinkedList<Quad>();
   private float size;
   private float kerneling;
   private float scale;
   private float spacing;

   public GText(GFont gFont, float size, float kerneling) {
      this.gFont = gFont;
      this.size = size;
      this.kerneling = kerneling;

      BlendState bs = renderer.createBlendState();
      bs.setBlendEnabled(true);
      bs.setSourceFunction(SourceFunction.SourceAlpha);
      bs.setDestinationFunction(DestinationFunction.OneMinusSourceAlpha);
      bs.setEnabled(true);
      setRenderState(bs);

      setText(text);
   }

   private void construct() {
      scale = size / gFont.getMetricsHeights();
      spacing = 0;
      for (int i = 0; i < charQuads.size(); i++) {
         if (i < text.length()) {
            float positionX = spacing * scale
                  + gFont.getMetricsWidths()[text.charAt(i)] * scale / 2f;
            float positionY = gFont.getTextDescent() * scale;
            charQuads.get(i).getLocalTranslation().setX(positionX);
            charQuads.get(i).getLocalTranslation().setY(positionY);

            float sizeX = size;
            float sizeY = size;
            charQuads.get(i).getLocalScale().setX(sizeX);
            charQuads.get(i).getLocalScale().setY(sizeY);

            if (fill != null) {
               charQuads.get(i).setSolidColor(fill);
            }
            attachChild(charQuads.get(i));

            charQuads.get(i).setRenderState(gFont.getChar(text.charAt(i)));
            spacing += gFont.getMetricsWidths()[text.charAt(i)] + kerneling;
         }
      }
      for (int i = charQuads.size(); i < text.length(); i++) {
         Quad quad = new Quad(String.valueOf(text.charAt(i)), 1f, 1f);

         float positionX = spacing * scale
               + gFont.getMetricsWidths()[text.charAt(i)] * scale / 2f;
         float positionY = gFont.getTextDescent() * scale;
         quad.getLocalTranslation().setX(positionX);
         quad.getLocalTranslation().setY(positionY);

         float sizeX = size;
         float sizeY = size;
         quad.getLocalScale().setX(sizeX);
         quad.getLocalScale().setY(sizeY);

         if (fill != null) {
            quad.setSolidColor(fill);
         }
         attachChild(quad);

         quad.setRenderState(gFont.getChar(text.charAt(i)));
         spacing += gFont.getMetricsWidths()[text.charAt(i)] + kerneling;

         charQuads.add(quad);
      }
      for (int j = text.length(); j < charQuads.size(); j++) {
         detachChild(charQuads.get(j));
      }
      for (int j = text.length(); j < charQuads.size(); j++) {
         charQuads.remove(j);
      }

      this.width = spacing * scale;
      this.height = size;
      updateRenderState();
   }

   public void setText(Object text) {
      this.text = String.valueOf(text);
      construct();
   }

   public String getText() {
      return text;
   }

   public void setFill(ColorRGBA fill) {
      this.fill = fill;
      construct();
   }

   public ColorRGBA getFill() {
      return fill;
   }
}



and the gui base class, which everything else extends.. if you don't want to use it, the GText class cand very easily be modified (just extend the Node class, and not the GBaseElement class)
but i think it could be useful to many, as it handles some very needed features like mouse released, double clicking or drag:

package gen.gui;

import com.jme.input.KeyInput;
import com.jme.input.MouseInput;
import com.jme.intersection.PickResults;
import com.jme.intersection.TrianglePickResults;
import com.jme.math.Ray;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.TriMesh;
import com.jme.system.DisplaySystem;
import com.jme.util.Timer;

/**
 *
 * @author Victor Porof, blue_veek@yahoo.com
 */
public class GBaseElement extends Node {

   /**
    *
    */
   private static final long serialVersionUID = 1L;
   protected Timer timer = Timer.getTimer();
   protected KeyInput keyInput = KeyInput.get();
   protected MouseInput mouseInput = MouseInput.get();
   protected DisplaySystem display = DisplaySystem.getDisplaySystem();
   protected Renderer renderer = display.getRenderer();
   protected Camera camera = renderer.getCamera();
   protected float width;
   protected float height;

   public float getWidth() {
      return width;
   }

   public float getHeight() {
      return height;
   }

   private int cMouseButton;
   private int delay;
   private static int delayMax = 5;
   private boolean startTimer;
   private boolean mousePressed;
   private boolean mouseReleased;
   private boolean mousePressed2;
   private boolean mouseReleased2;

   public void update() {
      if (mouseReleased) {
         mouseReleased = false;
         mousePressed = false;
      } else if (mouseInput.isButtonDown(cMouseButton)) {
         mousePressed = true;
      }
      if (isMouseReleased(cMouseButton) || startTimer) {
         if (mouseReleased2) {
            mouseReleased2 = false;
            mousePressed2 = false;
         } else if (mouseInput.isButtonDown(cMouseButton)) {
            mousePressed2 = true;
         }

         startTimer = true;
         if (startTimer) {
            delay++;
         }
         if (delay > delayMax) {
            startTimer = false;
            delay = 0;
         }
      }
   }

   private float mposX, mposY, iposX, iposY, deltaX, deltaY;
   private boolean moving;
   private static boolean currentMovingTrigger;
   private static boolean globalMovingTrigger = !currentMovingTrigger;

   public void drag(boolean event) {
      if (event) {
         if (currentMovingTrigger != globalMovingTrigger) {
            if (isMouseOver()) {
               moving = true;
               globalMovingTrigger = currentMovingTrigger;

               mposX = mouseInput.getXAbsolute();
               iposX = getLocalTranslation().x;
               mposY = mouseInput.getYAbsolute();
               iposY = getLocalTranslation().y;
            }
         }
         if (moving && currentMovingTrigger == globalMovingTrigger) {
            deltaX = mposX - mouseInput.getXAbsolute();
            deltaY = mposY - mouseInput.getYAbsolute();
            getLocalTranslation().set(iposX - deltaX, iposY - deltaY, 0);
         }
      } else {
         moving = false;
         currentMovingTrigger = false;
         globalMovingTrigger = !currentMovingTrigger;
      }
   }

   public boolean isMouseOver() {
      if (getRenderQueueMode() != Renderer.QUEUE_ORTHO) {
         if (isPick() != null) {
            return true;
         } else {
            return false;
         }
      } else {
         if (mouseInput.getXAbsolute() >= getLocalTranslation().x - width
               / 2f
               && mouseInput.getXAbsolute() <= getLocalTranslation().x
                     + width / 2f
               && mouseInput.getYAbsolute() >= getLocalTranslation().y
                     - height / 2f
               && mouseInput.getYAbsolute() <= getLocalTranslation().y
                     + height / 2f) {
            return true;
         } else {
            return false;
         }
      }
   }

   public boolean isMousePressed(int cMouseButton) {
      this.cMouseButton = cMouseButton;
      return mouseInput.isButtonDown(cMouseButton) && isMouseOver();
   }

   public boolean isMouseReleased(int cMouseButton) {
      this.cMouseButton = cMouseButton;
      if (!mouseInput.isButtonDown(cMouseButton) && mousePressed
            && isMouseOver()) {
         mouseReleased = true;
         return true;
      } else {
         return false;
      }
   }

   public boolean isMouseDoubleClicked(int cMouseButton) {
      this.cMouseButton = cMouseButton;
      if (!mouseInput.isButtonDown(cMouseButton) && mousePressed2
            && isMouseOver()) {
         mouseReleased2 = true;
         return true;
      } else {
         return false;
      }
   }

   private PickResults pickResults;
   private Ray ray;
   private Vector2f mousePosition;
   private Vector3f worldCoords;
   private Vector3f[] vert;
   private Vector3f intersection;
   private TriMesh target;

   public Vector3f isPick() {
      pickResults = new TrianglePickResults();
      pickResults.setCheckDistance(true);
      pickResults.clear();

      mousePosition = new Vector2f(mouseInput.getXAbsolute(), mouseInput
            .getYAbsolute());
      worldCoords = display.getWorldCoordinates(mousePosition, 0);

      ray = new Ray();
      ray.setOrigin(camera.getLocation());
      ray.setDirection(worldCoords.subtractLocal(camera.getLocation()));

      findPick(ray, pickResults);

      if (pickResults.getNumber() > 0) {
         intersection = new Vector3f();
         vert = new Vector3f[3];
         try {
            target = (TriMesh) pickResults.getPickData(0).getTargetMesh();
            for (int i = 0; i < target.getTriangleCount(); i++) {
               target.getTriangle(i, vert);
               if (ray.intersectWhere(vert[0].addLocal(target
                     .getWorldTranslation()), vert[1].addLocal(target
                     .getWorldTranslation()), vert[2].addLocal(target
                     .getWorldTranslation()), intersection)) {
                  break;
               }
            }
         } catch (Exception e) {
         }
      } else {
         intersection = null;
      }

      return intersection;
   }
}



here's a screenshot, along with another gui object - a shadowed container (as i said, soon i shall post the complete gui code):


use it like this:
you might want to disable lights as well if you don't need them for the text..

GFont font = new GFont(new Font("Arial", Font.BOLD, 64)) // to be more pretty, use a larger font
GText label = new GText (font, 28, 1); // the font, new size and extra spacing
label.setLocalTranslation(100, 100, 0);
label.setRenderQueueMode(Renderer.QUEUE_ORTHO);
label.setText("the quick bla bla bla...");
label.setFill(new ColorRGBA(0, 0.75f, 0, 1));
rootNode.attachChild(label);

rootNode.updateGeometricState(0.0f, true); // not necessary
rootNode.updateRenderState();



EDIT: forgot to mention, it's gpl

That looks good, does it support newlines?

I just noticed, that jme's Text doesn't currently.

Core-Dump said:

That looks good, does it support newlines?
I just noticed, that jme's Text doesn't currently.


no, but the text container class does that.