TrueType Font Support

I'm guessing people may not need this, as I'm seeing others talking about 3D fonts and whatever. But here's my first attempt at porting one of my custom tools over to jME (and please be nice, I've only been using jME for 2 days now). Here it is, FontTT.java:



Please feel free to help me optimize it for jME. My old tool would let you pick the font color on the fly, but I haven't figured out quite how to do that with jME yet (you basically just need to add a color to the node).



/*
 * Updated on June 21, 2006
 *
 * Written by Jeremy Adams
 *
 */
package tools;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.HashMap;

import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

/**
 * @author Jeremy Adams (elias4444)
 *
 */
public class FontTT {

   private Texture[] charactersp, characterso;
   private HashMap<String, IntObject> charlistp = new HashMap<String, IntObject>();
   private HashMap<String, IntObject> charlisto = new HashMap<String, IntObject>();
   private int kerneling;
   private int fontsize = 32;
   private Font font;
   private class IntObject {
      public int charnum;
      IntObject(int charnumpass) {
         charnum = charnumpass;
      }
   }

   public FontTT(Font font, int fontresolution, int extrakerneling) {

      this.kerneling = extrakerneling;
      this.font = font;
      fontsize = fontresolution;

      TextureState.forceNonPowerOfTwoTextureSizeUsage();

      createPlainSet();
      createOutlineSet();
   }

   private BufferedImage getFontImage(char ch) {
      Font tempfont;
      tempfont = font.deriveFont((float)fontsize);
      //Create a temporary image to extract font size
      BufferedImage tempfontImage = new BufferedImage(1,1, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g = (Graphics2D)tempfontImage.getGraphics();
      g.setFont(tempfont);
      FontMetrics fm = g.getFontMetrics();
      int charwidth = fm.charWidth(ch);

      if (charwidth <= 0) {
         charwidth = 1;
      }
      int charheight = fm.getHeight();
      if (charheight <= 0) {
         charheight = fontsize;
      }

      //Create another image for texture creation
      BufferedImage fontImage;
      fontImage = new BufferedImage(charwidth,charheight, BufferedImage.TYPE_INT_ARGB);
      Graphics2D gt = (Graphics2D)fontImage.getGraphics();
      gt.setFont(tempfont);

      //gt.setColor(Color.RED);
      //gt.fillRect(0, 0, charwidth, fontsize);
      gt.setColor(Color.WHITE);
      int charx = -fm.getLeading();
      int chary = 0;
      gt.drawString(String.valueOf(ch), (charx), (chary) + fm.getAscent());

      return fontImage;

   }


   private BufferedImage getOutlineFontImage(char ch) {
      Font tempfont;
      tempfont = font.deriveFont((float)fontsize);

      //Create a temporary image to extract font size
      BufferedImage tempfontImage = new BufferedImage(1,1, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g = (Graphics2D)tempfontImage.getGraphics();
      g.setFont(tempfont);
      FontMetrics fm = g.getFontMetrics();
      int charwidth = fm.charWidth(ch);

      if (charwidth <= 0) {
         charwidth = 1;
      }
      int charheight = fm.getHeight();
      if (charheight <= 0) {
         charheight = fontsize;
      }

      //Create another image for texture creation
      int ot = (int)((float)fontsize/24f);

      BufferedImage fontImage;
      fontImage = new BufferedImage(charwidth + 2*ot,charheight + 2*ot, BufferedImage.TYPE_INT_ARGB);
      Graphics2D gt = (Graphics2D)fontImage.getGraphics();
      gt.setFont(tempfont);

      //gt.setColor(Color.RED);
      //gt.fillRect(0, 0, charwidth, fontsize);
      gt.setColor(Color.WHITE);
      int charx = -fm.getLeading() + ot;
      int chary = ot;
      gt.drawString(String.valueOf(ch), (charx) + ot, (chary) + fm.getAscent());
      gt.drawString(String.valueOf(ch), (charx) - ot, (chary) + fm.getAscent());
      gt.drawString(String.valueOf(ch), (charx), (chary) + ot + fm.getAscent());
      gt.drawString(String.valueOf(ch), (charx), (chary) - ot + fm.getAscent());
      gt.drawString(String.valueOf(ch), (charx) + ot, (chary) + ot + fm.getAscent());
      gt.drawString(String.valueOf(ch), (charx) + ot, (chary) - ot + fm.getAscent());
      gt.drawString(String.valueOf(ch), (charx) - ot, (chary) + ot + fm.getAscent());
      gt.drawString(String.valueOf(ch), (charx) - ot, (chary) - ot + fm.getAscent());

      float ninth = 1.0f / 9.0f;
      float[] blurKernel = {
           ninth, ninth, ninth,
           ninth, ninth, ninth,
           ninth, ninth, ninth
      };
      BufferedImageOp blur = new ConvolveOp(new Kernel(3, 3, blurKernel));

      BufferedImage returnimage = blur.filter(fontImage, null);

      return returnimage;

   }


   
   
   private void createPlainSet() {
      charactersp = new Texture[256];

      for(int i=0;i<256;i++) {
         char ch = (char)i;

         BufferedImage fontImage = getFontImage(ch);

         charactersp[i] = TextureManager.loadTexture(fontImage, Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, true);

         charlistp.put(String.valueOf(ch), new IntObject(i));

         fontImage = null;
      }

   }
   
   private void createOutlineSet() {
      characterso = new Texture[256];

      for(int i=0;i<256;i++) {
         char ch = (char)i;

         BufferedImage fontImage = getOutlineFontImage(ch);

         characterso[i] = TextureManager.loadTexture(fontImage, Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, true);

         charlisto.put(String.valueOf(ch), new IntObject(i));

         fontImage = null;
      }

   }

   public Node createText(String text, float size, ColorRGBA color, boolean centered) {
      float fontsizeratio = size/fontsize;
      
      Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer();
      
      
      Node returnnode = new Node();

      int tempkerneling = (int)(kerneling * fontsizeratio);
      int k = 0;

      float startwidth;
      if (centered) {
         startwidth = -(getWidth(text, size))/2f;
      } else {
         startwidth = 0;
      }
      for (int i=0; i < text.length(); i++) {
         String tempstr = text.substring(i,i+1);

         Quad tempquad;
         float mywidth;
         k = ((charlistp.get(tempstr))).charnum;
         tempquad = new Quad(tempstr,charactersp[k].getImage().getWidth()*fontsizeratio,charactersp[k].getImage().getHeight()*fontsizeratio);
         mywidth = charactersp[k].getImage().getWidth() * fontsizeratio;

         if (i == 0) {
            tempquad.setLocalTranslation(new Vector3f(startwidth, 0, 0));
         } else {
            Quad lastquad = (Quad)returnnode.getChild(returnnode.getQuantity() - 1);
            float tempx =  (lastquad.getCenter()).x + ( ((BoundingBox)(lastquad.getWorldBound())).xExtent) + mywidth/2f + tempkerneling;
            Vector3f tempvec = new Vector3f(tempx,0,0);
            tempquad.setLocalTranslation(tempvec);
         }


         TextureState ts = renderer.createTextureState();
         ts.setEnabled(true);
         ts.setTexture(charactersp[k]);
         tempquad.setRenderState(ts);

         tempquad.setDefaultColor(color);

         tempquad.setModelBound(new BoundingBox());
         tempquad.updateModelBound();

         returnnode.attachChild(tempquad);

      }

      AlphaState as1 = renderer.createAlphaState();
      as1.setBlendEnabled( true );
      as1.setSrcFunction( AlphaState.SB_SRC_ALPHA );
      as1.setDstFunction( AlphaState.DB_ONE_MINUS_SRC_ALPHA );
      as1.setEnabled( true );

      returnnode.setRenderState(as1);

      returnnode.setLightCombineMode(LightState.OFF);

      return returnnode;

   }


   public Node createOutlinedText(String text, float size, ColorRGBA color, ColorRGBA outlinecolor, boolean centered) {
      float fontsizeratio = size/fontsize;
      
      Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer();
      
      if (color.a < outlinecolor.a) {
         outlinecolor.a = color.a;
      }
      
      Node returnnode = new Node();

      int tempkerneling = (int)(kerneling * fontsizeratio);
      int k = 0;

      float startwidth;
      if (centered) {
         startwidth = -(getWidth(text, size))/2f;
      } else {
         startwidth = 0;
      }
      for (int i=0; i < text.length(); i++) {
         String tempstr = text.substring(i,i+1);

         Quad tempquad;
         Quad tempquadoutline;
         float mywidth;
         k = ((charlistp.get(tempstr))).charnum;
         tempquad = new Quad(tempstr,charactersp[k].getImage().getWidth()*fontsizeratio,charactersp[k].getImage().getHeight()*fontsizeratio);
         tempquadoutline = new Quad(tempstr,characterso[k].getImage().getWidth()*fontsizeratio,characterso[k].getImage().getHeight()*fontsizeratio);
         mywidth = charactersp[k].getImage().getWidth() * fontsizeratio;

         if (i == 0) {
            tempquad.setLocalTranslation(new Vector3f(startwidth, 0, 0));
            tempquadoutline.setLocalTranslation(new Vector3f(startwidth, 0, -0.01f));
         } else {
            Quad lastquad = (Quad)returnnode.getChild(returnnode.getQuantity() - 2);
            float tempx =  (lastquad.getCenter()).x + ( ((BoundingBox)(lastquad.getWorldBound())).xExtent) + mywidth/2f + tempkerneling;
            Vector3f tempvec = new Vector3f(tempx,0,0);
            tempquad.setLocalTranslation(tempvec);
            tempquadoutline.setLocalTranslation(new Vector3f(tempx, 0, -0.01f));
         }

         TextureState ts1 = renderer.createTextureState();
         ts1.setEnabled(true);
         ts1.setTexture(characterso[k]);
         tempquadoutline.setRenderState(ts1);
         
         tempquadoutline.setDefaultColor(outlinecolor);
         tempquadoutline.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
         tempquadoutline.setModelBound(new BoundingBox());
         tempquadoutline.updateModelBound();

         returnnode.attachChild(tempquadoutline);

         TextureState ts = renderer.createTextureState();
         ts.setEnabled(true);
         ts.setTexture(charactersp[k]);
         tempquad.setRenderState(ts);

         tempquad.setDefaultColor(color);
         
         tempquad.setModelBound(new BoundingBox());
         tempquad.updateModelBound();
         
         returnnode.attachChild(tempquad);

      }

      AlphaState as1 = renderer.createAlphaState();
      as1.setBlendEnabled( true );
      as1.setSrcFunction( AlphaState.SB_SRC_ALPHA );
      as1.setDstFunction( AlphaState.DB_ONE_MINUS_SRC_ALPHA );
      as1.setEnabled( true );

      returnnode.setRenderState(as1);

      returnnode.setLightCombineMode(LightState.OFF);

      return returnnode;

   }


   public int getWidth(String whatchars, float size) {
      float fontsizeratio = size/fontsize;

      int tempkerneling = (int)(kerneling*fontsizeratio);
      float totalwidth = 0;
      int k = 0;
      for (int i=0; i < whatchars.length(); i++) {
         String tempstr = whatchars.substring(i,i+1);
         k = ((charlistp.get(tempstr))).charnum;
         totalwidth += charactersp[k].getImage().getWidth()*fontsizeratio + tempkerneling;
      }
      return (int)totalwidth;

   }



}

Ok, I updated the code at the top of this thread again. Now it'll do better translucency and support black. I still can't get it to work in the TestThirdPersonController example… but it it works in everything else I've thrown at it.  :stuck_out_tongue:

Sounds great.  Is there a test class (sorry, not in a position to download right this moment.)

Cool, could you upload an image of what it looks like ? The idea with the Font3D is to also make a general interface for fonts/texts. That way it is fairly easy to swap one implementation of fonts/text with an other, and easy for users of JME to make their own font-implementation. I suggest you look at the JmeText/JmeTextFactory interfaces and see if you can implement those. Please let me know if there are things about this interfaces that you thing is badly designed.

Are you talking about com.jmex.font3d.TextFactory.java?

BTW, here’s the screenshot you asked for:



Extremely impressive knock-my-socks-off screenshot there elias. :-p



darkfrog

Yeah, I do my best.  :smiley:



BTW, I've updated the code up top. It now includes support for coloring the text as well.

Here’s a new picture… just for you Darkfrog.  :wink:



good work! :slight_smile:

great! this is just what i needed  :smiley:

…I turned my speakers on but I couldn't hear the music. :-p



(BTW, much more impressive)



darkfrog

It looks great, I am sorry for the late reply. Yes it was the TextFactory/JmeText interfaces I was talking about. I have made the Text3D/Font3D and Text2D/Font2D pairs, and was thinking it be best to gather these different font/text representations under one common set of interfaces.



Hopefully we could also get your implementation under these interfaces, hence streamlining fonts/texts for the users of JME. Thereby also making it easier to submit a new implementation for someone who want to contribute :slight_smile:

I've been struggling trying to convert the code over to your interfaces. I've run into a couple of problems:


  1. By creating textured quads for each character, I can't find a way to use JmeText. I probably just don't understand JmeText, but I keep running into one roadblock after another.


  2. To better utilize the ideology behind the interfaces, I have to preload textures for all possibilities (a set for plain, a set for bold, a set for italic). And I have to make them large enough to shrink them down and look nice. This takes up a bit of texture memory, and also is pretty slow to initialize.  :frowning:



    Feel free to go through the code and see what you can do, but until I can better understand those interfaces, I'm afraid I'm at a loss.

Ok, it's not converted over to those interfaces, but I've tried to put it more in line with the jME mentality. I updated the code in the first post to reflect that. I figure, it takes a few seconds to build all the textures on my machine, but since it should all be done in the preloading phase, it shouldn't hurt too much.



Also, note the option to specify the font resolution now too. I've found that anything around 64 is really nice.


Good job! You're running into some of the same problems I did. I just couldn't get Text to understand kerneling on a per character basis. You can't hard code it either, as truetype fonts come in all different shapes and sizes.  :expressionless:


the size problem lies in AWT. fonts use dpi sizes, not pixel sizes. and i didn't found any utliity in awt to nicely transform from dpi to pixel size.



EDIT: btw, text uses hardcoded values for sizes and advances. and what exactly is kerneling?

Kerneling is the process of shifting characters so they're flush against the characters around them. Monospace fonts don't use kerneling, so they're equally spaced apart no matter how much space a specific character takes. To do the kerneling with the Truetype fonts, I would render each one on it's own quad, check the width, and then flush the next one up against it. This could be done because when I rendered the fonts, I make the texture only as wide as the character by using fontMetrics.

The text interface didn't even exist a few weeks ago, if you think it can be changed for the good…

lol… and here I am, just starting, assuming that it was an integral part of the engine from the beginning of time.  :stuck_out_tongue:



I'll take a look at it.