Yet another AWT Billboard text label

A few people have requested help with creating text like the playernames in this project. So here’s my code. If you decide to use it be aware that it has a few weak spots, such as not enforcing power of two textures or creating a dummy BufferedImage to obtain font metrics  - but it works, and is easy to use (it better be, because I haven’t had time to comment it :D)

You can add a playername to your player simply by doing

playerNode.attachChild(new TextLabel2D("Player").getBillboard(1f));


(Of course you might want to translate the label to float above your player, but that is left as an exercise to the reader)

[EDIT]Textures are now created with power of two dimensions, which fixes the warning message you got on most graphics cards for not using correct texture formats and a display problem with some fonts on linux[/EDIT]


import com.jme.image.Texture;
import com.jme.math.FastMath;
import com.jme.math.Vector2f;
import com.jme.renderer.Renderer;
import com.jme.scene.BillboardNode;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.*;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jme.util.geom.BufferUtils;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
import java.nio.FloatBuffer;
import java.util.Arrays;

public class TextLabel2D {
    private String text;
    private float blurIntensity = 0.1f;
    private int kernelSize = 5;
    private ConvolveOp blur;
    private Color foreground = new Color(1f, 1f, 1f);
    private Color background = new Color(0f, 0f, 0f);
    private float fontResolution = 40f;
    private int shadowOffsetX = 2;
    private int shadowOffsetY = 2;
    private Font font;
   
    public TextLabel2D(String text) {
        this.text = text;
        updateKernel();
        setFont(Font.decode("Sans PLAIN 40"));
    }
   
    public void setFont(Font font){
        this.font = font;
    }
   
    public void setShadowOffsetX(int offsetPixelX){
        shadowOffsetX = offsetPixelX;
    }
    public void setShadowOffsetY(int offsetPixelY){
        shadowOffsetY = offsetPixelY;
    }
    public void setBlurSize(int kernelSize){
        this.kernelSize = kernelSize;
        updateKernel();
    }
    public void setBlurStrength(float strength){
        this.blurIntensity = strength;
        updateKernel();
    }
    public void setFontResolution(float fontResolution){
        this.fontResolution = fontResolution;
    }

    private void updateKernel() {
        float[] kernel = new float[kernelSize*kernelSize];
        Arrays.fill(kernel, blurIntensity);
        blur = new ConvolveOp(new Kernel(kernelSize, kernelSize, kernel));
    }
   
    /**
     *
     * @param scaleFactors is set to the factors needed to adjust texture coords
     * to the next-power-of-two- sized resulting image
     */
    private BufferedImage getImage(Vector2f scaleFactors){
        BufferedImage tmp0 = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = (Graphics2D) tmp0.getGraphics();
        Font drawFont = font.deriveFont(fontResolution);
        g2d.setFont(drawFont);
        Rectangle2D b = g2d.getFontMetrics().getStringBounds(text, g2d);
       
        int actualX = (int)b.getWidth()+kernelSize+1+shadowOffsetX;
        int actualY = (int)b.getHeight()+kernelSize+1+shadowOffsetY;
       
        int desiredX = FastMath.nearestPowerOfTwo(actualX);
        int desiredY = FastMath.nearestPowerOfTwo(actualY);
       
        if(scaleFactors != null){
            scaleFactors.x = (float)actualX/desiredX;
            scaleFactors.y = (float)actualY/desiredY;
        }
       
        tmp0 = new BufferedImage(desiredX, desiredY, BufferedImage.TYPE_INT_ARGB);
       
        g2d = (Graphics2D) tmp0.getGraphics();
        g2d.setFont(drawFont);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
       
        int textX = kernelSize/2;
        int textY = g2d.getFontMetrics().getMaxAscent() - kernelSize/2;
       
        g2d.setColor(background);
        g2d.drawString(text, textX + shadowOffsetX, textY + shadowOffsetY);
       
        BufferedImage ret = blur.filter(tmp0, null);
       
        g2d = (Graphics2D) ret.getGraphics();
        g2d.setFont(drawFont);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
       
        g2d.setColor(foreground);
        g2d.drawString(text, textX, textY);
       
        return ret;
    }
   
    public Quad getQuad(float height){
        Vector2f scales = new Vector2f();
        BufferedImage img = getImage(scales);
        float w = img.getWidth() * scales.x;
        float h = img.getHeight() * scales.y;
        float factor = height / h;
        Quad ret = new Quad("textLabel2d", w * factor , h * factor);
        TextureState ts = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
        Texture tex = TextureManager.loadTexture(img, Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, true);
       
        FloatBuffer texCo = ret.getTextureBuffer(0, 0);
        FloatBuffer newTC = BufferUtils.createFloatBuffer(texCo.limit());
        texCo.rewind();
        for(int i=0; i<texCo.limit(); i+=2){
            float u = texCo.get();
            float v = texCo.get();
            newTC.put(u*scales.x);
            newTC.put(v*scales.y);
        }
        ret.setTextureBuffer(0, newTC);
        ret.updateGeometricState(0, true);
       
//        tex.setScale(new Vector3f(scales.x, scales.y, 1));
        ts.setTexture(tex);
        ts.setEnabled(true);
        ret.setRenderState(ts);
       
        ret.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
       
        AlphaState as = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
        as.setBlendEnabled(true);
        as.setTestEnabled(true);
        as.setTestFunction(AlphaState.TF_GREATER);
        as.setEnabled(true);
        ret.setRenderState(as);
       
        ret.setLightCombineMode(LightState.OFF);
        ret.updateRenderState();
        return ret;
    }
   
    public BillboardNode getBillboard(float height){
        BillboardNode bb = new BillboardNode("bb");
        Quad q = getQuad(height);
        bb.attachChild(q);
        return bb;
    }

    public void setForeground(Color foreground) {
        this.foreground = foreground;
    }

    public void setBackground(Color background) {
        this.background = background;
    }
}

Sweet - that dropped into my game and worked like a charm! Thanks!

Thanks heevee.  I'll give this a shot when I get home.

amazing…

just dropped it in and worked like a charm…

thank you very very much…

maybe one of the 34243 billboard text nodes out there should be included into jme? so far, i've seen about 5 (and mine)

Thanks.

jme 2 version.

i just commented the Texture coords stuff out for now, i don't think its really needed is it?



import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.Arrays;

import com.jme.image.Texture;
import com.jme.image.Texture.MagnificationFilter;
import com.jme.image.Texture.MinificationFilter;
import com.jme.math.FastMath;
import com.jme.math.Vector2f;
import com.jme.renderer.Renderer;
import com.jme.scene.BillboardNode;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.BlendState.TestFunction;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

public class TextLabel2D {
    private String text;
    private float blurIntensity = 0.1f;
    private int kernelSize = 5;
    private ConvolveOp blur;
    private Color foreground = new Color(1f, 1f, 1f);
    private Color background = new Color(0f, 0f, 0f);
    private float fontResolution = 40f;
    private int shadowOffsetX = 2;
    private int shadowOffsetY = 2;
    private Font font;
   
    public TextLabel2D(String text) {
        this.text = text;
        updateKernel();
        setFont(Font.decode("Sans PLAIN 40"));
    }
   
    public void setFont(Font font){
        this.font = font;
    }
   
    public void setShadowOffsetX(int offsetPixelX){
        shadowOffsetX = offsetPixelX;
    }
    public void setShadowOffsetY(int offsetPixelY){
        shadowOffsetY = offsetPixelY;
    }
    public void setBlurSize(int kernelSize){
        this.kernelSize = kernelSize;
        updateKernel();
    }
    public void setBlurStrength(float strength){
        this.blurIntensity = strength;
        updateKernel();
    }
    public void setFontResolution(float fontResolution){
        this.fontResolution = fontResolution;
    }

    private void updateKernel() {
        float[] kernel = new float[kernelSize*kernelSize];
        Arrays.fill(kernel, blurIntensity);
        blur = new ConvolveOp(new Kernel(kernelSize, kernelSize, kernel));
    }
   
    /**
     *
     * @param scaleFactors is set to the factors needed to adjust texture coords
     * to the next-power-of-two- sized resulting image
     */
    private BufferedImage getImage(Vector2f scaleFactors){
        BufferedImage tmp0 = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = (Graphics2D) tmp0.getGraphics();
        Font drawFont = font.deriveFont(fontResolution);
        g2d.setFont(drawFont);
        Rectangle2D b = g2d.getFontMetrics().getStringBounds(text, g2d);
       
        int actualX = (int)b.getWidth()+kernelSize+1+shadowOffsetX;
        int actualY = (int)b.getHeight()+kernelSize+1+shadowOffsetY;
       
        int desiredX = FastMath.nearestPowerOfTwo(actualX);
        int desiredY = FastMath.nearestPowerOfTwo(actualY);
       
        if(scaleFactors != null){
            scaleFactors.x = (float)actualX/desiredX;
            scaleFactors.y = (float)actualY/desiredY;
        }
       
        tmp0 = new BufferedImage(desiredX, desiredY, BufferedImage.TYPE_INT_ARGB);
       
        g2d = (Graphics2D) tmp0.getGraphics();
        g2d.setFont(drawFont);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
       
        int textX = kernelSize/2;
        int textY = g2d.getFontMetrics().getMaxAscent() - kernelSize/2;
       
        g2d.setColor(background);
        g2d.drawString(text, textX + shadowOffsetX, textY + shadowOffsetY);
       
        BufferedImage ret = blur.filter(tmp0, null);
       
        g2d = (Graphics2D) ret.getGraphics();
        g2d.setFont(drawFont);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
       
        g2d.setColor(foreground);
        g2d.drawString(text, textX, textY);
       
        return ret;
    }
   
    public Quad getQuad(float height){
        Vector2f scales = new Vector2f();
        BufferedImage img = getImage(scales);
        float w = img.getWidth() * scales.x;
        float h = img.getHeight() * scales.y;
        float factor = height / h;
        Quad ret = new Quad("textLabel2d", w * factor , h * factor);
        TextureState ts = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
        Texture tex = TextureManager.loadTexture(img, MinificationFilter.BilinearNoMipMaps, MagnificationFilter.Bilinear, true);
       
//        TexCoords texCo = ret.getTextureCoords(0);
//        texCo.coords = BufferUtils.createFloatBuffer(16);
//        texCo.coords.rewind();
//        for(int i=0; i < texCo.coords.limit(); i+=2){
//            float u = texCo.coords.get();
//            float v = texCo.coords.get();
//            texCo.coords.put(u*scales.x);
//            texCo.coords.put(v*scales.y);
//        }
//        ret.setTextureCoords(texCo);
//        ret.updateGeometricState(0, true);
       
//        tex.setScale(new Vector3f(scales.x, scales.y, 1));
        ts.setTexture(tex);
        ts.setEnabled(true);
        ret.setRenderState(ts);
       
        ret.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
       
        BlendState as = DisplaySystem.getDisplaySystem().getRenderer().createBlendState();
        as.setBlendEnabled(true);
        as.setTestEnabled(true);
        as.setTestFunction(TestFunction.GreaterThan);
        as.setEnabled(true);
        ret.setRenderState(as);
       
        ret.setLightCombineMode(LightCombineMode.Off);
        ret.updateRenderState();
        return ret;
    }
   
    public BillboardNode getBillboard(float height){
        BillboardNode bb = new BillboardNode("bb");
        Quad q = getQuad(height);
        bb.attachChild(q);
        return bb;
    }

    public void setForeground(Color foreground) {
        this.foreground = foreground;
    }

    public void setBackground(Color background) {
        this.background = background;
    }
}




and a small test:


import java.awt.Color;

import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.math.Vector3f;
import com.jme.scene.BillboardNode;
import com.jme.scene.Node;
import com.jme.scene.shape.Box;

public class TextLabel2DTest extends SimpleGame {
   public static void main(String[] args) {
      TextLabel2DTest game = new TextLabel2DTest();
      game.setConfigShowMode(ConfigShowMode.AlwaysShow);
      game.start();
   }
   @Override
   protected void simpleInitGame() {
      for (int i = 0; i < 5; i++) {
         Node n = new Node("Node " +i);
         Box b = new Box("b" +i, new Vector3f(), 0.5f, 0.2f, 0.5f);
         b.setModelBound(new BoundingBox());
         b.updateModelBound();
         n.attachChild(b);
         rootNode.attachChild(n);
         
         TextLabel2D label = new TextLabel2D("Box :" +i);
         label.setBackground(Color.blue);
         BillboardNode bNode = label.getBillboard(0.5f);
         bNode.getLocalTranslation().y += 1;
         n.attachChild(bNode);
         n.setLocalTranslation(i, i, i);
      }
   }
}

The texture coords stuff is necessary to prevent distortion caused by always using power of trwo sized textures.

Any clue how to convert the commented section to jME2?  So far it works fine with the comments, but I haven't tested extensively.

I am using this. It gives you a quad which center is centered on the text baseline, horizontally centered.



package net.jgf.jme.scene.util;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.Arrays;

import com.jme.image.Texture;
import com.jme.image.Texture.MagnificationFilter;
import com.jme.image.Texture.MinificationFilter;
import com.jme.math.FastMath;
import com.jme.math.Vector2f;
import com.jme.renderer.Renderer;
import com.jme.scene.BillboardNode;
import com.jme.scene.TexCoords;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.BlendState.TestFunction;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

public class TextQuadUtils {

   private String text;

   private float blurIntensity = 0.1f;

   private int kernelSize = 5;

   private ConvolveOp blur;

   private Color foreground = new Color(1f, 1f, 1f);

   private Color background = new Color(0f, 0f, 0f);

   private float fontResolution = 40f;

   private int shadowOffsetX = 2;

   private int shadowOffsetY = 2;

   private Font font = Font.decode(DEFAULT_FONT);

   //public static final String DEFAULT_FONT = "Verdana PLAIN 40";
   public static final String DEFAULT_FONT = "Sans PLAIN 40";

   public TextQuadUtils(String text) {
      this.text = text;
      updateKernel();
   }

   public void setFont(Font font) {
      this.font = font;
   }

   public void setFont(String fontString) {
      this.font = Font.decode(fontString);
   }

   public void setShadowOffsetX(int offsetPixelX) {
      shadowOffsetX = offsetPixelX;
   }

   public void setShadowOffsetY(int offsetPixelY) {
      shadowOffsetY = offsetPixelY;
   }

   public void setBlurSize(int kernelSize) {
      this.kernelSize = kernelSize;
      updateKernel();
   }

   public void setBlurStrength(float strength) {
      this.blurIntensity = strength;
      updateKernel();
   }

   public void setFontResolution(float fontResolution) {
      this.fontResolution = fontResolution;
   }

   private void updateKernel() {
      float[] kernel = new float[kernelSize * kernelSize];
      Arrays.fill(kernel, blurIntensity);
      blur = new ConvolveOp(new Kernel(kernelSize, kernelSize, kernel));
   }

   /**
    *
    * @param scaleFactors is set to the factors needed to adjust texture coords
    * to the next-power-of-two- sized resulting image
    */
   private BufferedImage getImage(Vector2f scaleFactors) {
      BufferedImage tmp0 = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g2d = (Graphics2D) tmp0.getGraphics();
      Font drawFont = font.deriveFont(fontResolution);
      g2d.setFont(drawFont);
      Rectangle2D b = g2d.getFontMetrics().getStringBounds(text, g2d);

      int actualX = (int) b.getWidth() + kernelSize + 1 + Math.abs(shadowOffsetX);
      int actualY = (int) b.getHeight() + kernelSize + 1
            + Math.abs(shadowOffsetY);

      int desiredX = FastMath.nearestPowerOfTwo(actualX);
      int desiredY = FastMath.nearestPowerOfTwo(actualY);

      if (scaleFactors != null) {
         scaleFactors.x = (float) actualX / desiredX;
         scaleFactors.y = (float) actualY / desiredY;
      }

      tmp0 = new BufferedImage(desiredX, desiredY, BufferedImage.TYPE_INT_ARGB);

      g2d = (Graphics2D) tmp0.getGraphics();
      g2d.setFont(drawFont);
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

      int textX = kernelSize / 2;
      int textY = g2d.getFontMetrics().getMaxAscent() - kernelSize / 2;

      g2d.setColor(background);
      g2d.drawString(text, textX + shadowOffsetX, textY + shadowOffsetY);

      BufferedImage ret = blur.filter(tmp0, null);

      g2d = (Graphics2D) ret.getGraphics();
      g2d.setFont(drawFont);
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

      g2d.setColor(foreground);
      g2d.drawString(text, textX, textY);

      return ret;
   }

   public Quad getQuad(float height) {
      Vector2f scales = new Vector2f();
      BufferedImage img = getImage(scales);
      float w = img.getWidth() * scales.x;
      float h = img.getHeight() * scales.y;
      float factor = height / h;
      Quad ret = new Quad("textLabel2d", w * factor, h * factor);
      TextureState ts = DisplaySystem.getDisplaySystem().getRenderer()
            .createTextureState();
      Texture tex = TextureManager.loadTexture(img,
            MinificationFilter.BilinearNoMipMaps, MagnificationFilter.Bilinear,
            true);

      TexCoords texCo = ret.getTextureCoords(0);
      //texCo.coords = BufferUtils.createFloatBuffer(texCo.coords.limit());
      texCo.coords.rewind();
      for (int i = 0; i < texCo.coords.limit(); i += 2) {
         float u = texCo.coords.get(i);
         float v = texCo.coords.get(i + 1);
         texCo.coords.put(i, u * scales.x);
         texCo.coords.put(i + 1, v * scales.y);
      }
      ret.setTextureCoords(texCo);
      ret.updateGeometricState(0, true);

      //tex.setScale(new Vector3f(scales.x, scales.y, 1));
      ts.setTexture(tex);
      ts.setEnabled(true);
      ret.setRenderState(ts);

      ret.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
      ret.updateGeometricState(0, true);

      BlendState as = DisplaySystem.getDisplaySystem().getRenderer()
            .createBlendState();
      as.setBlendEnabled(true);
      as.setTestEnabled(true);
      as.setTestFunction(TestFunction.GreaterThan);
      as.setEnabled(true);
      ret.setRenderState(as);

      ret.setLightCombineMode(LightCombineMode.Off);
      ret.updateRenderState();
      return ret;
   }

   public BillboardNode getBillboard(float height) {
      BillboardNode bb = new BillboardNode("bb");
      Quad q = getQuad(height);
      bb.attachChild(q);
      return bb;
   }

   public void setForeground(Color foreground) {
      this.foreground = foreground;
   }

   public void setBackground(Color background) {
      this.background = background;
   }
}

hevee:



I also just "dropped it in" and it worked for me, too. Just saying thank you for saving me hours.



I get a small problem with clipping in the verticle direction, though, but this is so minor its hard to notice.

For example an "L" and other tall letters will have their tops cut off (An O might start to look like an U).








Interesting. Are you using it on a linux system? I have seen a similar problem on ubuntu, but never bothered enough to fix it. I wish I could now, but I don't think I'll have enough time before next year…

Ubuntu Linux is exactly what I'm using.



But I've fixed this problem, by changing these lines, Instead of 40:


private float fontResolution = 48f;
setFont(Font.decode("Sans PLAIN 48"));


A comment about 'powers of two' tipped me off to try this. Instead of something just divisible by two.

I tried to extend the TextQuadUtils class by adding multiline support, but it works not so good because I don't know how to set the size of the quad correctly.



Please help!!!




import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.Arrays;

import com.jme.image.Texture;
import com.jme.image.Texture.MagnificationFilter;
import com.jme.image.Texture.MinificationFilter;
import com.jme.math.FastMath;
import com.jme.math.Vector2f;
import com.jme.renderer.Renderer;
import com.jme.scene.BillboardNode;
import com.jme.scene.TexCoords;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.BlendState.TestFunction;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

public class TextQuadUtils {

   private String text;

   private float blurIntensity = 0.1f;

   private int kernelSize = 5;

   private ConvolveOp blur;

   private Color foreground = new Color(1f, 1f, 1f);

   private Color background = new Color(0f, 0f, 0f);

   private float fontResolution = 40f;

   private int shadowOffsetX = 2;

   private int shadowOffsetY = 2;

   private Font font = Font.decode(DEFAULT_FONT);
   
   private boolean drawShadow = false;

   //public static final String DEFAULT_FONT = "Verdana PLAIN 40";
   public static final String DEFAULT_FONT = "Sans PLAIN 40";

   public TextQuadUtils(String text) {
      this.text = text;
      updateKernel();
   }

   public void setFont(Font font) {
      this.font = font;
   }

   public void setFont(String fontString) {
      this.font = Font.decode(fontString);
   }

   public void setShadowOffsetX(int offsetPixelX) {
      shadowOffsetX = offsetPixelX;
   }

   public void setShadowOffsetY(int offsetPixelY) {
      shadowOffsetY = offsetPixelY;
   }

   public void setBlurSize(int kernelSize) {
      this.kernelSize = kernelSize;
      updateKernel();
   }

   public void setBlurStrength(float strength) {
      this.blurIntensity = strength;
      updateKernel();
   }

   public void setFontResolution(float fontResolution) {
      this.fontResolution = fontResolution;
   }

   private void updateKernel() {
      float[] kernel = new float[kernelSize * kernelSize];
      Arrays.fill(kernel, blurIntensity);
      blur = new ConvolveOp(new Kernel(kernelSize, kernelSize, kernel));
   }

   /**
    *
    * @param scaleFactors is set to the factors needed to adjust texture coords
    * to the next-power-of-two- sized resulting image
    */
   private BufferedImage getImage(Vector2f scaleFactors) {
      BufferedImage tmp0 = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g2d = (Graphics2D) tmp0.getGraphics();
      Font drawFont = font.deriveFont(fontResolution);
      g2d.setFont(drawFont);
      Rectangle2D b = g2d.getFontMetrics().getStringBounds(text, g2d); // <--- THIS DOESNT WORK WITH LINE WRAPPING

      int actualX = (int) b.getWidth() + kernelSize + 1 + Math.abs(shadowOffsetX);
      int actualY = (int) b.getHeight() + kernelSize + 1
            + Math.abs(shadowOffsetY);

      int desiredX = FastMath.nearestPowerOfTwo(actualX);
      int desiredY = FastMath.nearestPowerOfTwo(actualY);

      if (scaleFactors != null) {
         scaleFactors.x = (float) actualX / desiredX;
         scaleFactors.y = (float) actualY / desiredY;
      }

      tmp0 = new BufferedImage(desiredX, desiredY, BufferedImage.TYPE_INT_ARGB);

      g2d = (Graphics2D) tmp0.getGraphics();
      g2d.setFont(drawFont);
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

      int textX = kernelSize / 2;
      int textY = g2d.getFontMetrics().getMaxAscent() - kernelSize / 2;

      if (drawShadow){
         g2d.setColor(background);
         g2d.drawString(text, textX + shadowOffsetX, textY + shadowOffsetY);
      }

      BufferedImage ret = blur.filter(tmp0, null);

      g2d = (Graphics2D) ret.getGraphics();
      g2d.setFont(drawFont);
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      
      g2d.setColor(foreground);
      // g2d.drawString(text, textX, textY);

      Point2D.Float pen = new Point2D.Float(textX, textY);

      FontRenderContext frc = g2d.getFontRenderContext();

      // let styledText be an AttributedCharacterIterator containing at least
      // one character
      AttributedString as = new AttributedString(text);
      as.addAttribute(TextAttribute.FONT, drawFont);
      AttributedCharacterIterator styledText = as.getIterator();

      LineBreakMeasurer measurer = new LineBreakMeasurer(styledText, frc);
      float wrappingWidth = 150; // here is the width for line wrapping

      while (measurer.getPosition() < text.length()) {

         TextLayout layout = measurer.nextLayout(wrappingWidth);

         pen.y += (layout.getAscent());
         float dx = layout.isLeftToRight() ? 0 : (wrappingWidth - layout
               .getAdvance());

         System.out.println(layout.toString());
         layout.draw(g2d, pen.x + dx, pen.y);
         pen.y += layout.getDescent() + layout.getLeading();
      }

      return ret;
   }

   public Quad getQuad(float height) {
      Vector2f scales = new Vector2f();
      BufferedImage img = getImage(scales);
      float w = img.getWidth() * scales.x;
      float h = img.getHeight() * scales.y;
      float factor = height / h;
      Quad ret = new Quad("textLabel2d", w * factor, h * factor);
      TextureState ts = DisplaySystem.getDisplaySystem().getRenderer()
            .createTextureState();
      Texture tex = TextureManager.loadTexture(img,
            MinificationFilter.BilinearNoMipMaps, MagnificationFilter.Bilinear,
            true);

      TexCoords texCo = ret.getTextureCoords(0);
      //texCo.coords = BufferUtils.createFloatBuffer(texCo.coords.limit());
      texCo.coords.rewind();
      for (int i = 0; i < texCo.coords.limit(); i += 2) {
         float u = texCo.coords.get(i);
         float v = texCo.coords.get(i + 1);
         texCo.coords.put(i, u * scales.x);
         texCo.coords.put(i + 1, v * scales.y);
      }
      ret.setTextureCoords(texCo);
      ret.updateGeometricState(0, true);

      //tex.setScale(new Vector3f(scales.x, scales.y, 1));
      ts.setTexture(tex);
      ts.setEnabled(true);
      ret.setRenderState(ts);

      ret.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
      ret.updateGeometricState(0, true);

      BlendState as = DisplaySystem.getDisplaySystem().getRenderer()
            .createBlendState();
      as.setBlendEnabled(true);
      as.setTestEnabled(true);
      as.setTestFunction(TestFunction.GreaterThan);
      as.setEnabled(true);
      ret.setRenderState(as);

      ret.setLightCombineMode(LightCombineMode.Off);
      ret.updateRenderState();
      return ret;
   }

   public BillboardNode getBillboard(float height) {
      BillboardNode bb = new BillboardNode("bb");
      Quad q = getQuad(height);
      bb.attachChild(q);
      return bb;
   }

   public void setForeground(Color foreground) {
      this.foreground = foreground;
   }

   public void setBackground(Color background) {
      this.background = background;
   }

   public void setText(String text) {
      this.text = text;
      updateKernel();
   }
   
   public void setDrawShadow(boolean drawShadow){
      this.drawShadow = drawShadow;
   }
}

3H, maybe you can use FontMetrics.getHeight() to find the height of a text line and multiply it yourself by the number of lines in the text. You would also get the highest width of all the lines you are going to print.

Ok, thx… I'm a really AWT noob, sorry for the stupid question. :roll:



Now I have a working solution for my case, but its not very common, so I won't post it here.

Thanks for the solution.

It saved a lot of time :wink:

Bit of an old topic at this point, but I'm assuming some people might still be using this so thought I'd post. I noticed that while changing the font resolution from 40 to 48 does indeed fix some of the font "clipping" in Ubuntu, it also appears to break this code under Windows7 x64 (the label won't appear at all).



-Prime

Oh, and here's how we handled the porting of the tex coord code:



          //New JME2 tex coord stuff /////////////////////////////////////////////////
          TexCoords texCo = ret.getTextureCoords(0);
          texCo.coords.rewind();
     for (int i = 0; i < texCo.coords.limit(); i += 2) {
       float u = texCo.coords.get(i);
       float v = texCo.coords.get(i + 1);
       texCo.coords.put(i, u * scales.x);
       texCo.coords.put(i + 1, v * scales.y);
     }
     ret.setTextureCoords(texCo);
     ret.updateGeometricState(0, true);
          ///////////////////////////////////////////////////////////////////

Hi!

I’m using this class calling the getBillboard method to label some boxes.

I discovered that as the text gets longer the label appears like right aligned (it is not centered anymore/tends to the left).

I tried to fix this by setting the localTranslation but this is very exhausting and not always works.

Does anyone have a suggestion to fix this right inside the TextLabel2D class?