2D Radar in NiftyGui Image Element

Hi folks,

I tried to find an example for a 2D radar which can be integrated into a NiftyGui image element but was unsuccessfull. A separate cam with its own viewport was not an option because you can’t see the 3D models with a dark texture from far away.
With the help of comments in these posts (Offscreen rendering with nifty gui?, Minimap and Minimap with NiftyGUI) I was able to compose a working solution. Maybe it helps someone out there. With the extra class “PaintedGauge” you’ll be able to paint a gauge in NiftyGui analog to the radar example.
You’ll find a working example on github.

PaintableImage abstract class:

import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;

public abstract class PaintableImage extends Image {
	private BufferedImage bufferedImage;
	private ByteBuffer scratch;
	Texture2D texture;

	public PaintableImage(int width, int height) {
		super();

		setFormat(Format.RGBA8);
		setWidth(width);
		setHeight(height);

		bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR_PRE);
		scratch = ByteBuffer.allocateDirect(4 * width * height);

		texture = new Texture2D(this);
		texture.setMinFilter(Texture.MinFilter.Trilinear);
		texture.setMagFilter(Texture.MagFilter.Bilinear);
	}

	public void refreshImage() {
		Graphics2D g = bufferedImage.createGraphics();
		paint(g);
		g.dispose();

		/* get the image data */
		byte data[] = (byte[]) bufferedImage.getRaster().getDataElements(0, 0, getWidth(), getHeight(), null);
		scratch.clear();
		scratch.put(data, 0, data.length);
		scratch.rewind();
		setData(scratch);
	}

	public Texture getTexture() {
		return texture;
	}

	protected abstract void paint(Graphics2D graphicsContext);
}

PaintedRadar class:

import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import net.carriercommander.objects.PlayerUnit;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;

public class PaintedRadar extends PaintableImage {

	public static final int MIN_RANGE = 1000;
	public static final int MAX_RANGE = 10000;

	private final int offsetX, offsetY, radius;
	private final Ellipse2D radarCircle;
	private final Node rootNode;
	private PlayerUnit activeUnit = null;
	private int range = 3000;

	private final Color colorBackground = new Color(0f, 0f, 0f, 0f);
	private final Color colorCircle = Color.BLACK;
	private final Color colorPlayer = new Color(0f, 0.51f, 1f, 1f); // cyan for CC

	/**
	 * A 2D painted radar which can be inserted into a nifty gui image. Make sure that where the radar should appear
	 * the host image is not fully opaque - the gauge will be rendered behind the image.
	 *
	 * @param widthContainer The width of the container image - should match the picture width otherwise radar will get stretched
	 * @param heightContainer The heigth of the container image - should match the picture heigth otherwise radar will get stretched
	 * @param offsetX The x offset where the start point of the gauge will appear inside the host image
	 * @param offsetY The y offset where the start point of the gauge will appear inside the host image
	 * @param radius The radius of the radar to be painted
	 * @param rootNode The node whose children should be painted on the radar screen
	 */
	public PaintedRadar(int widthContainer, int heightContainer, int offsetX, int offsetY, int radius, Node rootNode) {
		super(widthContainer, heightContainer);
		this.rootNode = rootNode;
		this.radius = radius;
		this.offsetX = offsetX;
		this.offsetY = offsetY;
		radarCircle = new Ellipse2D.Double(offsetX - radius, offsetY - radius, radius * 2, radius * 2);
		refreshImage();
	}

	public void setActiveUnit(PlayerUnit activeUnit) {
		this.activeUnit = activeUnit;
	}

	public void setRange(int range) {
		if (range < MIN_RANGE) {
			range = MIN_RANGE;
		}
		if (range > MAX_RANGE) {
			range = MAX_RANGE;
		}
		if (range != this.range) {
			this.range = range;
			refreshImage();
		}
	}

	public void changeRange(int amount) {
		setRange(range + amount);
	}

	protected void paint(Graphics2D g) {
		g.setBackground(colorBackground);
		g.clearRect(0, 0, width, height);
		g.setColor(colorCircle);
		g.fill(radarCircle);

		rootNode.getChildren().stream()
				.filter(node -> !node.getClass().equals(Geometry.class))
				.forEach(node -> {
					if (activeUnit.getWorldTranslation().distance(node.getWorldTranslation()) <= range) {
						Vector3f radarCoordinates = node.getWorldTranslation().subtract(activeUnit.getWorldTranslation());

						Quaternion radarRotation = activeUnit.getWorldRotation().inverse();
						radarCoordinates = radarRotation.mult(radarCoordinates);
						g.setColor(colorPlayer);
						g.fillRect((int) (offsetX + radius * radarCoordinates.x / range), height - (int) (offsetY + radius * radarCoordinates.z / range), 2, 2);
					}
				});
	}
}

PaintedGauge class:

import java.awt.Color;
import java.awt.Graphics2D;

public class PaintedGauge extends PaintableImage {
	private final int widthGauge, heightGauge, offsetX, offsetY;
	private final boolean vertical;
	private float value = 0;

	private final Color colorGauge = new Color(0f, 0.51f, 1f, 1f);
	private final Color colorBackground = new Color(0f, 0f, 0f, 0.5f);

	/**
	 * A 2D painted gauge which can be inserted into a nifty gui image. Make sure that where the gauge should appear
	 * the host image is not fully opaque - the gauge will be rendered behind the image.
	 *
	 * @param widthContainer The width of the container image - should match the picture width otherwise gauge will get stretched
	 * @param heightContainer The heigth of the container image - should match the picture heigth otherwise gauge will get stretched
	 * @param offsetX The x offset where the start point of the gauge will appear inside the host image
	 * @param offsetY The y offset where the start point of the gauge will appear inside the host image
	 * @param widthGauge The desired width of the gauge bar
	 * @param heightGauge The desired height of the gauge bar
	 * @param vertical if true, the gauge will be displayed vertically, otherwise horizontally
	 */
	public PaintedGauge(int widthContainer, int heightContainer, int offsetX, int offsetY, int widthGauge, int heightGauge, boolean vertical) {
		super(widthContainer, heightContainer);
		this.offsetX = offsetX;
		this.offsetY = offsetY;
		this.widthGauge = widthGauge;
		this.heightGauge = heightGauge;
		this.vertical = vertical;
		refreshImage();
	}

	/**
	 * Set the value to be displayed by the gauge.
	 *
	 * @param value between 0 and 1
	 */
	public void setValue(float value) {
		if (value > 1) {
			value = 1;
		}
		if (value < 0) {
			value = 0;
		}
		if (value != this.value) {
			this.value = value;
			refreshImage();
		}
	}

	/**
	 * Paint the gauge with the defined parameters and value.
	 *
	 * @param g the graphics object to paint on - provided by PaintableImage.refreshImage().
	 */
	@Override
	protected void paint(Graphics2D g) {
		g.setBackground(colorBackground);
		g.clearRect(offsetX, offsetY, widthGauge, heightGauge);
		g.setColor(colorGauge);
		if (vertical) {
			g.fillRect(offsetX, offsetY, widthGauge, Math.round(value * heightGauge));
		} else {
			g.fillRect(offsetX, offsetY, Math.round(value * widthGauge), heightGauge);
		}
	}
}

In your HUD screen controller, you’ll have to add this:

	private PlayerAppState playerAppState = null;
	private PaintedRadar radar;
	private ImageRenderer radarRenderer = null;
	TextureKey radarTextureKey = new TextureKey("radarKey");
	private Node rootNode;

	private PaintedGauge fuelGauge;
	private ImageRenderer fuelGaugeRenderer = null;
	TextureKey fuelTextureKey = new TextureKey("fuelKey");

	@Override
	public void initialize(AppStateManager stateManager, Application app) {
		super.initialize(stateManager, app);
		...
		this.radar = new PaintedRadar(86, 90, 42, 46, 39, rootNode);
		this.fuelGauge = new PaintedGauge(42, 90, 30,7, 4, 74, true);
	}

	@Override
	public void update(float tpf) {
		if (System.currentTimeMillis() > timestamp + 200) {
			if (nifty != null && playerAppState != null && screen != null) {
				if (radarRenderer != null) {
					radar.refreshImage();
					replaceNiftyImage(radar, radarTextureKey, radarRenderer);
				}
				if (fuelGaugeRenderer != null) {
					fuelGauge.setValue((float)Math.random()); //TODO get the vehicle's fuel level somehow
					replaceNiftyImage(fuelGauge, fuelTextureKey, fuelGaugeRenderer);
				}
			}
		}
	}

	private void replaceNiftyImage(PaintableImage imageBuffer, TextureKey fuelTextureKey, ImageRenderer renderer) {
		app.getAssetManager().addToCache(fuelTextureKey, imageBuffer.getTexture());
		NiftyImage image = nifty.createImage(screen, fuelTextureKey.getName(), false);
		renderer.setImage(image);
	}

	// a method where you switch your active player unit - if you don't switch, just initialize the variables somewhere else
	public void switchActiveUnit() {
		Element radarImage = screen.findElementById("radar");
		radarRenderer = (radarImage != null ? radarImage.getRenderer(ImageRenderer.class) : null);
		radar.setActiveUnit(playerAppState.getActiveUnit());

		Element fuelImage = screen.findElementById("fuel");
		fuelGaugeRenderer = (fuelImage != null ? fuelImage.getRenderer(ImageRenderer.class) : null);
	}

	// when buttons to zoom in/out are pressed, see hud.xml
	public void radarZoom(String direction) {
		if ("in".equals(direction)) {
			this.radar.changeRange(-1000);
		} else {
			this.radar.changeRange(1000);
		}
	}

	// call immediately after HUD screen controller was instanciated to pass the rootNode of the scene graph
	public void attachScene(Node rootNode) {
		this.rootNode = rootNode;
	}

In your class which extends SimpleApplication:

	private void createNitfyGui() {
		...
		HudScreenControl hudScreenControl = (HudScreenControl) nifty.getScreen(Constants.SCREEN_HUD).getScreenController();
		hudScreenControl.attachScene(rootNode); // so the radar can display all direct children of the scene graph's root node
		...
	}

And last but not least the relevant parts in the hud.xml (the nifty screen definition XML). Note that the panel id (in this case “carrierRadar”) must correlate with the id in your screen controller (see screen.findElementById(“carrierRadar”))

	<panel childLayout="horizontal">
		<panel id="radar" childLayout="vertical">
			<image filename="Interface/Screens/hud/radarScreen.png" />
		</panel>
		<panel childLayout="vertical">
			<image filename="Interface/Screens/hud/zoomIn.png" style="unselected">
				<interact onClick="radarZoom(in)" />
			</image>
			<image filename="Interface/Screens/hud/zoomOut.png" style="unselected">
				<interact onClick="radarZoom(out)" />
			</image>
		</panel>
		<panel id="fuel" childLayout="horizontal">
			<image filename="Interface/Screens/hud/fuel.png" />
		</panel>
	</panel>

Hope this helps someone out there. If you think it could be done more efficiently, please let me know.

Cheers,
Michael

Edit 22.8.21: Improved the soltion. Now you’ll have to specify the width/height of the source image - if they don’t match, the rendered image will be stretched.

6 Likes