jME-TrueTypeFont Rendering Library

v1.25 is up. Fixed a bug that could cause recursive calls to StringContainer.getLines() to produce undefined results. This is something that could happen if you’re calling StringContainer.getLines() through a TTF_AtlasListener and the atlas was originally updated through StringContainer.getLines().

2 Likes

Uploaded v1.25a which doesn’t actually fix anything, just improves the recursive fix in 1.25 so that it’s more efficient by preventing StringContainer.getLines() from unnecessarily running its calculations multiple times.

@Ali_RS I also uploaded LemurDynamo Beta5 which renames the ComboBox to DropDown, because it’s actually a drop down menu, and adds an actual ComboBox. Also improved the Slider so that the increment/decrement buttons continually increment/decrement the slider value while pressed. The scroll rate and increment/decrement value can now be set via styles.

Also revamped the documentation with more notations, particularly for GradientBackgroundComponent and ShadowBackgroundComponent.

2 Likes

had to google a bit :>

1 Like

Does this new Advent theme remove dependency to groovy.jar ?

No it’s a theme that uses Groovy.

Yeah I haven’t made an official thread about it yet because there’s still more I wanna get done with it. I haven’t tested out any of the Android stuff yet at all, for all I know it might just crash hard when running on Android. Plus there’s another neat idea I think I wanna try out with LemurDynamo and jME-TrueTypeFont…

1 Like

If it works for Android it will be really good. :grinning:
In some thread someone had problem with compiling his lemur app for android. the problem was related to groovy stuffs i think.

1 Like

I think groovy could be a plugin on lemur. As I havent used it yet for anything, it seems not necessary.

I think the “glass” style and this new “advent” style use groovy.

1 Like

You only need the Groovy library if you plan on using Groovy style files such as glass-styles, razor-styles, advent-styles or writing your own styles. You can also just style your Elements at run-time in your code.

1 Like

I have updated jME-TrueTypeFont to version 1.26 which adds in the ability to apply a gaussian blur to your text. The new BlurText class takes in a StringContainer, which must have a textBox with width/height values, and produces a Texture2D with the blurred text.

In addition to this I have updated LemurDynamo to Beta6 which adds the ability to produced blurred drop shadows to your Labels and Buttons. LemurDynamo Beta6, obviously, requires jME-TrueTypeFont 1.26.

A few examples of blurred drop shadows in LemurDynamo Beta6:

getViewPort().setBackgroundColor(ColorRGBA.White);
GuiGlobals.initialize(this);
BaseStyles.loadAdventStyle();
GuiGlobals.getInstance().getStyles().setDefaultStyle("advent");

myWindow = new Container(new VBoxLayout(5, FillMode.Even, false));
guiNode.attachChild(myWindow);
myWindow.setLocalTranslation(100, 450, 0);

label = new Label("Hello World");
label.setFont(GuiGlobals.getInstance().loadFont("com/simsilica/lemur/style/base/fonts/Cantarell-Regular.ttf", Font.PLAIN, 42, 0));
label.setColor(new ColorRGBA(0.3f, 0.3f, 0.3f, 1));
label.setTextHAlignment(HAlignment.Center);
label.setTextVAlignment(VAlignment.Center);
label.setShadowPasses(3);
label.setShadowIntensity(1.5f);
label.setShadowColor(ColorRGBA.Black);
label.setShadowOffset(new Vector3f(3, -3, -1));
myWindow.addChild(label);


getViewPort().setBackgroundColor(ColorRGBA.White);
GuiGlobals.initialize(this);
BaseStyles.loadAdventStyle();
GuiGlobals.getInstance().getStyles().setDefaultStyle("advent");

myWindow = new Container(new VBoxLayout(5, FillMode.Even, false));
guiNode.attachChild(myWindow);
myWindow.setLocalTranslation(100, 450, 0);

label = new Label("Hello World");
label.setFont(GuiGlobals.getInstance().loadFont("com/simsilica/lemur/style/base/fonts/Cantarell-Regular.ttf", Font.PLAIN, 42, 0));
label.setColor(new ColorRGBA(1f, 1f, 1f, 1));
label.setTextHAlignment(HAlignment.Center);
label.setTextVAlignment(VAlignment.Center);
label.setShadowPasses(3);
label.setShadowIntensity(1.5f);
label.setShadowColor(ColorRGBA.Black);
label.setShadowOffset(new Vector3f(0, 0, -1));
myWindow.addChild(label);


getViewPort().setBackgroundColor(ColorRGBA.Black);
GuiGlobals.initialize(this);
BaseStyles.loadAdventStyle();
GuiGlobals.getInstance().getStyles().setDefaultStyle("advent");

myWindow = new Container(new VBoxLayout(5, FillMode.Even, false));
guiNode.attachChild(myWindow);
myWindow.setLocalTranslation(100, 450, 0);

label = new Label("Hello World");
label.setFont(GuiGlobals.getInstance().loadFont("com/simsilica/lemur/style/base/fonts/Cantarell-Regular.ttf", Font.PLAIN, 42, 0));
label.setColor(new ColorRGBA(0f, 0f, 0f, 1));
label.setTextHAlignment(HAlignment.Center);
label.setTextVAlignment(VAlignment.Center);
label.setShadowPasses(4);
label.setShadowIntensity(2.5f);
label.setShadowColor(new ColorRGBA(0.08f, 0.4f, 1, 1));
label.setShadowOffset(new Vector3f(0, 0, -1));
myWindow.addChild(label);

jME-TrueTypeFont: 1337 Gallery - jME-TTF
LemurDynamo: Dropbox - LemurDynamo-v1.02.zip - Simplify your life

P.S. In LemurDynamo blurred shadow options can also be set via styles. “shadowPasses” for the number of passes, “shadowRadiusMult” for the blur radius multipliler and “shadowIntensity” for the intensity.

Once turned on you can turn blurring off by setting the number of shadow passes to a value less than or equal to 0 or setting the radius multiplier to a value less than or equal to 0.

5 Likes

jME-TrueTypeFont has been updated to version 1.27 which improves character and line spacing calculations, offers a minor performance increase, adds in font scaling based on screen density and Android compatibility through Google’s Sfntly library. A pre-built Sfntly jar is available in the jME-TrueTypeFont zip file, however you may also build and use your own from https://github.com/googlei18n/sfntly. Sfntly is licensed under Apache 2.0.

Two noticeable differences where your code is concerned. The TrueTypeKey no longer takes a java.awt.Font argument for styling, such as java.awt.Font.PLAIN, instead you will now use truetypefont.util.Style such as truetypefont.util.Style.Plain. Additionally you may specify a DPI argument to the TrueTypeKey if you desire to take advantage of screen density scaling.

If you’re using Lemur Dynamo you’ll need to update to the newly uploaded Beta 7 which offers a few improvements and additions. Obviously it’s been updated to work with jME-TrueTypeFont v1.27, but also the android features have been updated and test to work. Lemur Dynamo now automatically detects if it is running on Android and adjusts accordingly. You can load fonts with dpi scaling using GuiGlobals.loadFontDP and you can load textures based on screen density categories via GuiGlobals.loadTextureDP.

In the case of the latter this works similarly to calling up assets from the Android API. For example if you’re loading the texture “Interface/Icons/myIcon.png” you will want to have several subfolders in “Interface/Icons” titled with density categories, namely:
ldpi
mdpi
hdpi
xhdpi
xxhdpi
xxxhdpi

When you request GuiGlobals.loadTextureDP for “Interface/Icons/myIcon.png” on an hdpi device it will first look for the texture in “Interface/Icons/hdpi/myIcon.png” and if not found it will try to locate it at “Interface/Icons/mdpi/myIcon.png” then “Interface/Icons/ldpi/myIcon.png” then “Interface/Icons/myIcon.png” then “Interface/Icons/xhdpi/myIcon.png” and so on. You store different size images in each folder for use on a device with the corresponding density category, the higher the density the bigger the texture. Check out com/simsilica/lemur/icons/Advent for an example of this also take a look at com.simsilica.lemur.style.base.AdventAndroidStyle.java.

In addition to that the GradientBackgroundComponent makes use of standard derivatives which are not guaranteed to be supported on all GLES devices. Lemur Dynamo will detect if the proper requirements are supported and if not standard derivatives will be disabled, GradientBackgroundComponent will still work, but the pseudo anti-aliasing will be disabled.

P.S. I will get around to updating my web-site with additional info in the not too distant future.

P.P.S. My phone supports standard derivatives and high precision floating point in the fragment shader yay, which is surprising given how incredibly cheap it was.

3 Likes

For anyone that may have downloaded version 1.27, I just uploaded a new zip file. No changes were made except that the bundled Sfntly jar has been replaced with one compiled with Java7 rather than Java8. Android isn’t compatible with Java8, however, when using Android Studio you can still use Java8 libraries if you use their new Jack and Jill compiler, but it doesn’t seem like that is an option when using the jME SDK so I re-compiled with Java7.

1 Like

Strange issue

Some of my texts render strangely (texts are in English)

Font i use : Consolas.ttf (tried also with others)
Using v1.27

For some of texts it shows correctly with same font but for some others shows like above.

my font creation method is same for all text .
Code :

TrueTypeKey ttk = new TrueTypeKey("Interface/Fonts/"+font.name()+".ttf",
                    truetypefont.util.Style.Plain, 40);
    TrueTypeFont ttf=(TrueTypeFont) assetManager.loadAsset(ttk);
                ttf.setScale(font.getSize()/40f);
        sc = new StringContainer(ttf);
                sc.setWrapMode(StringContainer.WrapMode.WordClip);
                sc.setVerticalAlignment(StringContainer.VAlign.Top);
                sc.setAlignment(StringContainer.Align.Center);
                sc.setTextBox(boundingBox);
                sc.setKerning(1);
                sc.setText(text);
                ttc = ttf.getFormattedText(sc, color);
                sc.getLines();
                ttc.updateGeometry();
               
                //The texture atlas may have been updated here so you'll need to update the texture on the material to reflect the changes. Consider adding a TTF_AtlasListener to the TrueTypeFont so you are notified when the atlas is modified.
                ttc.getMaterial().setTexture("Texture", ttf.getAtlas());

Any hint is appreciated.

Thanks

Couple points to note, first you don’t need:

sc.getLines();
ttc.updateGeometry();

the getLines() method is called by TrueTypeContainer.updateGeometry() and TrueTypeFont.getFormattedText. When you call TrueTypeFont.getFormattedText the TrueTypeContainer is created, you only need to call TrueTypeContainer.updateGeometry() if any of the parameters such as the text to display have changed after it is created.

Anyway, I downloaded consolas.ttf from http://www.fontpalace.com/font-details/Consolas/ and didn’t have any trouble with it.

If you download that one and don’t have trouble with it then maybe there’s something wrong with the TTF you have, maybe it doesn’t contain English characters? I downloaded a free font editor called Font Forge, you could grab that and take a look at the fonts you’re having trouble with. It should show you all the characters that the file has glyphs for.

If you have some free time you could even design your own sweet font :slight_smile:

1 Like

Tryder - this library claims to be a constrained Delaunay triangulation library. New BSD licensed. Could this potentially replace jDelaunay?

https://code.google.com/archive/p/jmescher/

2 Likes

Good find, I will look into it.

2 Likes

@Tryder

this is my code where it adds control to my spatial so when camera moves near it, it shows some text on GUI.

    private void initTestStations() {   
        notificationGUIMgr.registerNewNotifier(rootNode.getChild("TestStation_AI")).attachGUIMessage("ShowThisText", GUILocation.CENTER, 20);
        notificationGUIMgr.registerNewNotifier(rootNode.getChild("TestStation_BiDirectionalText")).attachGUIMessage("ShowThisText", GUILocation.CENTER, 20);` 
    }

when i set same text for both (“ShowThisText”) then both show text correctly but when i set different text for each one then the second one shows text correctly but ruins the text of first one.

NotificationGUIControl class :

public class NotificationGUIControl extends AbstractControl {

    private static NotificationGUIManager NOTIFICATION_GUI_MANAGER = null;
    private HashMap<String, Geometry> messageGeometries = new HashMap<>();
    private ResourceBundle messageBundle;
    private boolean isControlledWithCameraDistance = false;
    private boolean isControlledWithCameraLookDirection = false;
    private float distanceFromCamera;
    private Geometry currentMessage;
    private Camera cam;

    public NotificationGUIControl(AssetKey messageBundleKey) {
        messageBundle = ResourceBundle.getBundle(messageBundleKey.getName(), GameLevelApplication.LOCALE);     
    }

    public NotificationGUIControl(AssetKey messageBundleKey, Locale locale) {
        messageBundle = ResourceBundle.getBundle(messageBundleKey.getName(), locale);
    }

    @Override
    protected void controlUpdate(float tpf) {

        if (cam!=null && isControlledWithCameraDistance && currentMessage !=null) {
           
            if (cam.getLocation().distance(spatial.getWorldTranslation()) < distanceFromCamera) {
                
                if (isControlledWithCameraLookDirection) {
                    Vector3f dir = spatial.getWorldTranslation().subtract(cam.getLocation());
                    dir.normalizeLocal();
                    if (cam.getDirection().angleBetween(dir) <= 0.4) {
                        if (currentMessage.getParent() == null) {
                            NOTIFICATION_GUI_MANAGER.attachGUIMessage(currentMessage);
                            System.out.println("attached");
                        }
                        
                    }
                     else {
                        currentMessage.removeFromParent();
                        System.out.println("attached");
                    }
                } else if (currentMessage.getParent() == null) {
                    NOTIFICATION_GUI_MANAGER.attachGUIMessage(currentMessage);
                }

            } else {
                currentMessage.removeFromParent();
            }
        }
    }

    public static void setNotificationGUIMgr(NotificationGUIManager manager) {
        if (NotificationGUIControl.NOTIFICATION_GUI_MANAGER == null) {
            NotificationGUIControl.NOTIFICATION_GUI_MANAGER = manager;
        }

    }

    private Geometry generateGUINotificationMessage(String message, ColorRGBA color, String fontName, boolean isCenteral) {
        if (NOTIFICATION_GUI_MANAGER != null) {
            if (messageBundle.getLocale().getLanguage().equals(new Locale("fa").getLanguage()) || messageBundle.getLocale().getLanguage().equals(new Locale("ar").getLanguage())) {
                message = BiDiTTFTextConvertor.convert(message);
            }
            return NOTIFICATION_GUI_MANAGER.getTTFNotificationGUI().generateTTFNotificationMessage(message, color, fontName, isCenteral);
        } else {
            throw new NullPointerException("NotificationGUI Manager not running!");
        }
    }

    public void attachGUIMessage(String message, GUILocation location) {
        Geometry msg = messageGeometries.get(message);
        if (msg == null) {
            Random r=new Random();
            messageGeometries.put(message, generateGUINotificationMessage(message, ColorUtils.getFromString(messageBundle.getString(message + "_color")), messageBundle.getString(message + "_font"), Boolean.parseBoolean(messageBundle.getString(message + "_isCenteral"))));
            msg = messageGeometries.get(message);
        }
//        if (currentMessage != null) {
//            currentMessage.removeFromParent();
//        }
        NOTIFICATION_GUI_MANAGER.attachGUIMessage(msg, location);
        currentMessage = msg;

    }

    public void attachGUIMessage(String message, GUILocation location, float distance, boolean enableLookDirection) {
        Geometry msg = messageGeometries.get(message);
//        System.out.println(messageBundle.getString(message));
        if (msg == null) {
           
            //String messageText=messageBundle.getString(message);
            messageGeometries.put(message, generateGUINotificationMessage(message,ColorRGBA.Black,"Consolas",false));
            msg = messageGeometries.get(message);
        }
        currentMessage = msg;
        NOTIFICATION_GUI_MANAGER.attachGUIMessage(msg, location);
        setControlledWithCamera(true, distance, enableLookDirection);
        
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        if (cam == null) {
            this.cam = vp.getCamera();
        }

    }


    public void setControlledWithCamera(boolean enabled, float distance, boolean enableLookDirection) {
        isControlledWithCameraDistance = enabled;

        if (enabled) {
            isControlledWithCameraLookDirection = enableLookDirection;
            distanceFromCamera = distance;
        }
    }

}

TTFNotificationGUI class : (it generates text and return geometry of text)

public class TTFNotificationGUI {

    private int width, height;
    private HashMap<String, TrueTypeFont> ttfs;
    private StringContainer sc;
    private TrueTypeContainer ttc;

    public TTFNotificationGUI(int width, int height, AssetManager assetManager) {
        this.width = width;
        this.height = height;
        ttfs = new HashMap<>();
        for (Font font : Font.values()) {
            TrueTypeKey ttk = new TrueTypeKey("Interface/Fonts/" + font.name() + ".ttf",
                    truetypefont.util.Style.Plain, 40);
            TrueTypeFont ttf = (TrueTypeFont) assetManager.loadAsset(ttk);
            ttf.setScale(font.getSize() / 40f);
            ttfs.put(font.name(), ttf);
        }

    }

    public Geometry generateTTFNotificationMessage(String text, ColorRGBA color, String fontName, boolean isCenteral) {
        Rectangle boundingBox;
        if (isCenteral) {
            boundingBox = new Rectangle(0, 0, width / 2, height / 2);
        } else {
            boundingBox = new Rectangle(0, 0, width / 8, height / 8);
        }

        TrueTypeFont ttf = ttfs.get(fontName);
        if (ttf != null) {
            sc = new StringContainer(ttf);
            sc.setWrapMode(StringContainer.WrapMode.WordClip);
            sc.setVerticalAlignment(StringContainer.VAlign.Top);
            sc.setAlignment(StringContainer.Align.Center);
            sc.setTextBox(boundingBox);
            sc.setKerning(1);
            sc.setText(text);
            ttc = ttf.getFormattedText(sc, color);

            //The texture atlas may have been updated here so you'll need to update the texture on the material to reflect the changes. Consider adding a TTF_AtlasListener to the TrueTypeFont so you are notified when the atlas is modified.
            ttc.getMaterial().setTexture("Texture", ttf.getAtlas());
            ttc.setUserData("TextBoxWidth", sc.getTextBox().width);
            ttc.setUserData("TextHeight", sc.getTextHeight());
            return  ttc;
        } else {
            throw new NullPointerException("There is no font with name \"" + fontName + "\"");
        }
    }

}

I doubt this line ttc.getMaterial().setTexture("Texture", ttf.getAtlas()); may cause the problem. I tried to clone it for each geometry but not solved the problem.

Is there something shared in TrueTypeFont ttf ?

Edit : Yep, the problem is with Atlas in TruTypeFont is being shared. So when second control generates it’s text it modifies atlas. How can i clone atlas for each geometry then ? calling .clone() not helped. It seems it shares data yet after clone.

Edit2 : Could solve it using a deep cloner utile class for ByteBuffer to copy Image data in Texture2D. But it may be better to solve it internally using a flag to prevent disposing of atlas.

You need to add an atlas listener to the TrueTypeFont and update your text geometries when the atlas is updated.

TTF_AtlasListener listener = new TTF_AtlasListener() {
    @Override
    public void mod(AssetManager assetManager, int oldWidth,
            int oldHeight, int newWidth, int newHeight, TrueTypeFont font) {
        if (oldHeight != newHeight || oldWidth != newWidth)
            ttc.updateGeometry();
        
        ttc.getMaterial().setTexture("Texture", font.getAtlas());
    }
}
ttf.addAtlasListener(listener);

If you don’t want to mess with listeners you can pre-load all the characters you want to be able to use to the TrueTypeFont then lock the atlas to prevent any new characters from being added.

ttf.getBitmapGlyphs("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            + "abcdefghijklmnopqrstuvwxyz"
            + "0123456789!@#$%^&*()-_+=*/"
            + "\\:;\"<>,.?{}[]|`~'");
ttf.lockAtlas(true);

When a new character is added to the atlas it is recreated, the old texture is deleted and a new one created so you need to update your material to point to the new texture. However; if the atlas is not big enough to store whatever new characters are added then it is recreated at a larger resolution so the UV coordinates for all the other characters have changed so the materials and geometries need to be updated.

2 Likes

Wow, 100 posts already, nice.
I’ve got two questions:

1 - Do you support glyphs that consist of two chars (String’s length is 2 but codepoint count is 1)?
BitmapText does not support that (but it could easily be added by converting Strings to int-arrays).

2 - Do you support very large atlases? In BitmapFont it uses N textures instead of just one.

Happy coding,