Trying to create a scalable User Interface in Lemur - but the Icons and Font stay pixelated / unsharp

Hello,

I am in love with the jmonkeyengine so far - but I have a hard time with lemur :frowning:

What I am trying to do is creating a scalable User Interface, where the font and icons used always stay sharp independet of the Window Size / Screen Resolution, the scaling of the individual components works fine, but I am failing at the scaling of the Icons and font - i tried out multiple custom bitmap fonts and also differrent icon sizes (and tried playing with the scale or icon size settingsā€¦) - can someone of you help me out / explain this behaviour?

Also I am experiencing some strange behaviours like that lemur always positions elements from the bottom left Corner of the Screen and not from the upper left (I am kind of lost).

I am trying to rebuild this little 2d music project i wrote in 3D - https://vimeo.com/975848193

1 Like

Within its own containers, Lemur positions things from the upper left. jMonkeyEngine, openGL, most graphics frameworks, position things from the lower left. In fact, Lemur bucks the trend here by having its windows grow down instead of up so that within lemurā€™s space, you can mostly ignore the ā€œlower leftā€ problemā€¦ just start from a high y value and everything else will work as expected for a UI.

We would need to know more about what you mean, how you are scaling, etcā€¦ You havenā€™t even given us any visual representation of what you mean?

There could be any one of 1,247,116,723 things going on so itā€™s difficult to pick one or two to help with.

Oki doke, thank you for the heads up - here is a screenshot ā†’

An here is my current app state code:


import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.simsilica.lemur.*;
import com.simsilica.lemur.component.BorderLayout;
import com.simsilica.lemur.component.BoxLayout;
import com.simsilica.lemur.component.IconComponent;
import com.simsilica.lemur.component.QuadBackgroundComponent;
import com.simsilica.lemur.core.GuiComponent;
import com.simsilica.lemur.event.DefaultMouseListener;
import io.github.isaatonimov.humidiLab3.system.base.SimpleAppSystem;

import java.util.ArrayList;
import java.util.List;

public class MainUIState extends AbstractAppState implements SimpleAppSystem
{
	private Container mainWindow;
	private Container sidePanel;
	private Container buttonBar;
	private float lastScreenWidth;
	private float lastScreenHeight;
	private boolean alreadyRunning;
	private Camera cam;
	private Node guiNode;


	@Override
	public <T extends SimpleAppSystem> T initialize()
	{
		return (T) this;
	}

	@Override
	public void initialize(AppStateManager stateManager, Application app)
	{
		super.initialize(stateManager, app);

		cam 	= SystemsProperty.MainCamera();
		guiNode 	= SystemsProperty.App().getGuiNode();

		lastScreenWidth = cam.getWidth();
		lastScreenHeight = cam.getHeight();

		GuiGlobals.initialize(SystemsProperty.App());

		GuiGlobals.getInstance().getStyles().setDefaultStyle("glass");

		mainWindow = new Container(new BorderLayout());
		guiNode.attachChild(mainWindow);

		mainWindow.setLocalTranslation(0, cam.getHeight(), 0);
		mainWindow.setPreferredSize(new Vector3f(cam.getWidth(), cam.getHeight(),0));
		sidePanel = new Container(new BorderLayout());
		sidePanel.setPreferredSize(sizeInPercent(10, 100));
		sidePanel.setBackground(new QuadBackgroundComponent(ColorRGBA.fromRGBA255(0,0,0,0)));

		buttonBar = new Container(new BoxLayout());
		buttonBar.setPreferredSize(sizeInPercent(5, 100));

		buttonBar.addChild(new Button(""));
		buttonBar.addChild(new Button(""));
		buttonBar.addChild(new Button(""));
		buttonBar.addChild(new Button(""));
		buttonBar.addChild(new Button(""));
		buttonBar.addChild(new Button(""));
		buttonBar.addChild(new Button(""));
		buttonBar.addChild(new Button(""));

		for(var b : buttonBar.getChildren())
		{
			var button = ((Button)b);
			button.setPreferredSize(sizeInPercent(5, 5));
			button.setBackground(new QuadBackgroundComponent(ColorRGBA.randomColor()));
			button.setTextHAlignment(HAlignment.Center);
			button.setTextVAlignment(VAlignment.Center);
			button.setAlpha(1);

			var icon = new IconComponent("Icons/white/512/arrows.png");
			icon.setIconScale(0.1f);
			icon.setMargin(10, 10);
			button.setIcon(icon);
			button.addMouseListener(new DefaultMouseListener()
			{
				@Override
				public void mouseEntered(MouseMotionEvent event, Spatial target, Spatial capture)
				{
					super.mouseEntered(event, target, capture);
					button.setLocalScale(new Vector3f(1.1f, target.getLocalScale().y,target.getLocalScale().z));
					button.setAlpha(1);
				}

				@Override
				public void mouseExited(MouseMotionEvent event, Spatial target, Spatial capture)
				{
					super.mouseExited(event, target, capture);
					button.setLocalScale(new Vector3f(1f, target.getLocalScale().y,target.getLocalScale().z));
					button.setAlpha(1);
				}

				@Override
				public void mouseButtonEvent(MouseButtonEvent event, Spatial target, Spatial capture)
				{
					super.mouseButtonEvent(event, target, capture);

					if(event.isPressed())
						SystemsProperty.Audio().playRandomClickSound();
				}
			});
		}

		// Add some elements
		mainWindow.addChild(sidePanel, BorderLayout.Position.West);
		sidePanel.addChild(buttonBar, BorderLayout.Position.West);
	}

	public float fixedFontSize()
	{
		var size =  (cam.getWidth() + cam.getHeight()) / (25 * 4);
		System.out.println("Returning Font Size from Percent -> " + size);
		return size;
	}

	public Vector3f sizeInPercent(float widthPrc, float heightPrc)
	{
		var size = new Vector3f(widthPercentage(widthPrc), Math.min(40, heightPercentage(heightPrc)), 0);
		System.out.println("Returning Size from Percent -> " + size);
		return size;
	}

	public float widthPercentage(float percentage)
	{
		return (cam.getWidth() / 100) * percentage;
	}

	public float heightPercentage(float percentage)
	{
		return (cam.getHeight() / 100) * percentage;
	}


	public List<Label> getAllLabels(Panel inContainer)
	{
		var allNodes = new ArrayList<Label>();

		for(var spatial : inContainer.getChildren())
		{
			if(spatial instanceof Label)
			{
				allNodes.add((Label) spatial);
			}
			else if (spatial instanceof Panel)
			{
				if(spatial instanceof TabbedPanel)
				{
					for(var tab : ((TabbedPanel)spatial).getTabs())
					{
						allNodes.addAll(getAllLabels(tab.getContents()));
					}

					for(var tab : ((TabbedPanel)spatial).getTabs())
					{
						allNodes.addAll(getAllLabels(tab.getTitleButton()));
					}
				}
				else
					allNodes.addAll(getAllLabels((Panel) spatial));
			}
		}

		System.out.println("Searched through container -> count="+allNodes.size());

		return allNodes;
	}

	public List<GuiComponent> getAllIcons(Panel inContainer)
	{
		var allNodes = new ArrayList<GuiComponent>();

		for(var spatial : inContainer.getChildren())
		{
			if(spatial instanceof Button)
			{
				allNodes.add(((Button) spatial).getIcon());
			}
			else if (spatial instanceof Panel)
			{
				allNodes.addAll(getAllIcons((Panel) spatial));
			}
		}

		System.out.println("Searched through container -> count="+allNodes.size());

		return allNodes;
	}

	public void ifWindowSizeChangedRunTheFollowing(Runnable context)
	{
		if(cam != null)
		{
			if(!alreadyRunning && cam.getWidth() != lastScreenWidth || cam.getHeight() != lastScreenHeight)
			{
				alreadyRunning = true;
				System.out.println("Window Size Changed -> Running context");
				context.run();
			}
		}
	}

	@Override
	public void update(float tpf)
	{
		super.update(tpf);

		ifWindowSizeChangedRunTheFollowing(() ->
		{
			if(mainWindow != null)
			{
				mainWindow.setLocalTranslation(0, cam.getHeight(), 0);
				mainWindow.setPreferredSize(new Vector3f(cam.getWidth(), cam.getHeight(),0));

				for(var label : getAllLabels(mainWindow))
				{
					if(label != null)
					{
						label.setPreferredSize(sizeInPercent(100, 10));
						label.setFontSize(fixedFontSize());
					}
				}

				if(sidePanel != null)
				{
					sidePanel.setPreferredSize(sizeInPercent(20, 100));
				}

				if(buttonBar != null)
				{
					buttonBar.setPreferredSize(sizeInPercent(5, 100));

					for(var b : buttonBar.getChildren())
					{
						var button = ((Button)b);
						button.setPreferredSize(sizeInPercent(5, 5));
					}
				}

				if(cam != null)
				{
					lastScreenWidth = cam.getWidth();
					lastScreenHeight = cam.getHeight();
					alreadyRunning = false;
				}
			}
		});
	}
}

What is the effect you want to end up with?

A UI that scales perfectly with screen size?

Layouts that scale perfectly but buttons shrink to give more space as screen size goes up? (For example, UI elements will shrink but they will still be at the edges of the screen and stuff.)

Personally, I do a little bit of both. I pick an ā€œideal screen sizeā€ where I will make sure my layouts look the best and then scale up and down from there by setting the local scale of a root node to which I attach my GUI (I can also slide that root node so that 0,0 is the upper left).

As far as your icons go, I donā€™t really know if I see the issue. Normally, these icon textures will be loaded with the mag filter enabled but perhaps a default has changed or something.

You could try changing:

var icon = new IconComponent("Icons/white/512/arrows.png");

To:

Texture texture = assetManager.loadTexture("Icons/white/512/arrows.png");
var icon = new IconComponent(texture);

,and that would let you experiment with the mag filter of the texture.

As far as bitmap text scaling, thatā€™s all on JME. You have to generate fonts in the sizes that will look best. (This is why I pick an idealized screen size so that I can make my fonts+icons look good at that resolution and then let JME scale the whole UI up and down from there. These days, I think height=900 is my base size and I scale by camera.getHeight()/900)

3 Likes

Thank you for the very quick response - I will try to make it work from a base scale and then scale it down like you do.

i basically want the sidebar of the game to stick to the left side of the screen - just the width should scale / so it has a fixed width but at all resolutions. I will try out playing with the mag filter of the icons and update when I find a solution - you gave me a good idea how this could work - thank you very much!

I try to implement something like an anchoring system - like javafxs anchor pane

And good luck with the Mythruna project - which looks awesome :slight_smile:

I am a bit blind and use a very low screen resolution, i am feeling stupid - sry :confused:

Of course the icons looked pixelated :frowning:

Note: if you just want to anchor stuff to the sides of the screen then you can make a root container (with no background) and set its preferred size to the scaled screen sizeā€¦ and give it a border layout.

I use this approach for my debug HUD:

ā€¦it allows me to add components to any side of the screen.

2 Likes

@pspeed Hello again - I wrote a simple Anchor Layout

Here is a screenshot of how it looks like currently:

The Left and Right Sided Windows use the fixed Scale type
The Centered Windows and Windows inside Windows use the Percent Scale Type

The Ratio Scale type forms the windows / containers the same way the lwjgl window does preview it

Here are the positions and scale types that it supports currently:

Scale
{
	PIXEL,
	PERCENT,
	RATIO
}

Position
{
	CENTER_CENTER,
	TOP_CENTER,
	BOTTOM_CENTER,
	LEFT_CENTER,
	RIGHT_CENTER,
	LEFT_TOP,
	LEFT_BOTTOM,
	RIGHT_TOP,
	RIGHT_BOTTOM,
	DYNAMIC
}

The dynamic Position is for setting the anchored nodes to custom positions e.g for dragging

it is not finished yet - but my plan is to also implement a simple vbox layout and an hbox layout (but i guess the included layouts already work like that)

Yeah, SpringGridLayout can already do hbox or vbox.

Just a note that normally in a Lemur layout, the layout is responsible for the ā€œcellā€ but the element is responsible for where it is in that cell using insets. For example, all n/w/s/e style positioning can be done with a DynamicInsetsComponent and is more flexible than just nwse style positioning.

Oh well, i guess i should have gone more deeply throught the source - when fixing the mentioned above and sticking to the recommendations - would an anchor layout be beneficial to lemur?

I would like to contribute something to jme or one of the contributions in the futureā€¦

1 Like