java.lang.IllegalStateException: Scene graph is not properly updated for rendering

I am getting this error because I am updating node from separate thread.

Apr 28, 2023 7:19:41 PM com.jme3.app.LegacyApplication handleError
SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
java.lang.IllegalStateException: Scene graph is not properly updated for rendering.
State was changed after rootNode.updateGeometricState() call.
Make sure you do not modify the scene from another thread!
Problem spatial name: Root Node
at com.jme3.scene.Spatial.checkCulling(Spatial.java:367)
at com.jme3.renderer.RenderManager.renderSubScene(RenderManager.java:792)
...
Apr 28, 2023 7:19:41 PM com.jme3.system.JmeSystemDelegate lambda$new$0
WARNING: JmeDialogsFactory implementation not found.
Uncaught exception thrown in Thread[jME3 Main,5,main]
IllegalStateException: Scene graph is not properly updated for rendering.
State was changed after rootNode.updateGeometricState() call.
Make sure you do not modify the scene from another thread!
Problem spatial name: Root Node

Here is my code. I have used separate thread to show how nodes are added one by one at some interval. How to achieve the same without separate thread?
I tried Thread.sleep without introducing separate thread but it dosen’t help.


import java.awt.DisplayMode;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapText;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.Arrow;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.GridPanel;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.Panel;
import com.simsilica.lemur.TextField;
import com.simsilica.lemur.component.BorderLayout;
import com.simsilica.lemur.grid.ArrayGridModel;
import com.simsilica.lemur.style.BaseStyles;

public class Main extends SimpleApplication {

	public static void main(String[] args) {
		Main app = new Main();

		app.fullscreen(new AppSettings(true));
		app.start();
		app.restart();

	}

	public void fullscreen(AppSettings settings) {
		GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
		DisplayMode[] modes = device.getDisplayModes();
		int i = modes.length - 1; // note: there are usually several, let's pick the first
		settings.setResolution(modes[i].getWidth(), modes[i].getHeight());
		settings.setFrequency(modes[i].getRefreshRate());
		settings.setBitsPerPixel(modes[i].getBitDepth());
		settings.setFullscreen(true);
		setSettings(settings);

	}

	private Grid grid = new Grid();
	private int gridSize = 1;
	private SecureRandom dice = new SecureRandom();
	private ArrayList<Tile> defaultTiles = new ArrayList<>();
	private Node GEOM_NODE = new Node("GEOM_NODE");
	private TextField gridSizeSetting;

	private void loadTiles() {
		try {
			String path = "C:\\Users\\Pankaj\\Documents\\JME_Projs\\WFCTest\\src\\main\\resources\\mapping.json";
			Gson gson = new Gson();
			Type tileListType = new TypeToken<ArrayList<Tile>>() {
			}.getType();
			defaultTiles = gson.fromJson(new FileReader(path), tileListType);
		} catch (JsonIOException | JsonSyntaxException | FileNotFoundException e) {
			e.printStackTrace();
		}
	}

	private void clear() {
		defaultTiles.clear();
		grid.clear();
		GEOM_NODE.detachAllChildren();
	}

	private void renderPattern() {
		clear();
		loadTiles();
		try {
			gridSize = Integer.parseInt(gridSizeSetting.getText().trim());
		} catch (Exception e) {
		}
		setAssociation(defaultTiles);
		Tile tile = new Tile(defaultTiles.get(0));
		loadNode(tile);
		grid.add(tile);
		addNeighbours(tile);
		// render(table.getAllTiles());
	}

	private void plotTiles() {
		Point p = new Point();
		defaultTiles.forEach((t) -> {
			t.getPosition().setLocation(p);
			loadNode(t);
			if (p.y <= 5)
				p.y += 1;
			else {
				p.y = 0;
				p.x += 1;
			}

		});
		render(defaultTiles);
	}

	private void render(Collection<Tile> tiles) {
		tiles.forEach((_tile) -> {
			transformNode(_tile);
			GEOM_NODE.attachChild(_tile.getNode());
		});
	}

	private void loadNode(Tile tile) {
		Spatial geom = assetManager.loadModel(tile.getModel());
		if (tile.getRotation() > 0) {
			geom.setLocalRotation(getRotation(tile.getRotation()));
		}
		// attachCoordinateAxes((Node) geom);
		Node node = new Node(tile.getName());
		node.attachChild(geom);
		addText(tile.getName(), node);
		tile.setNode(node);

	}

	private void addNeighbours(Tile tile) {

		Map<Integer, Point> openPositions = grid.findOpenPositions(tile.getPosition());
		List<Tile> tmp = new LinkedList<>();
		for (Map.Entry<Integer, Point> positions : openPositions.entrySet()) {
			int side = positions.getKey();
			if (!isUnderGrid(side, tile)) {
				continue;
			}
			Map<Integer, Tile> neighbours = grid.findNeighbours(positions.getValue());
			Set<Tile> connections = new HashSet<>(4);
			neighbours.forEach((oppositSide, _tile) -> {
				if (connections.isEmpty()) {
					connections.addAll(_tile.getConnections(_tile.getOppositIndex(oppositSide)));
				} else {
					connections.retainAll(_tile.getConnections(_tile.getOppositIndex(oppositSide)));
				}
			});
			
			if (!connections.isEmpty()) {
				Tile n = new Tile(connections.toArray(new Tile[connections.size()])[dice.nextInt(connections.size())]);
				n.getPosition().setLocation(positions.getValue());
				grid.add(n);
				loadNode(n);
				tmp.add(n);
			}
		}
		tmp.forEach((t) -> addNeighbours(t));
		transformNode(tile);
		GEOM_NODE.attachChild(tile.getNode());
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private final float tileSize = 0.3048f * 2;
	private void transformNode(Tile tile) {
		Point point = tile.getPosition();
		Vector3f trans = tile.getNode().getLocalTranslation();
		tile.getNode().setLocalTranslation(trans.addLocal(tileSize * point.x, 0.0f, tileSize * point.y));
	}

	List<BitmapText> texts = new ArrayList<>(1000);

	private void addText(String text, Node node) {
		BitmapText textNode = new BitmapText(guiFont);
		textNode.setSize(guiFont.getCharSet().getRenderedSize());
		textNode.setText(text);
		textNode.setLocalTranslation(0, 0.2f, 0);
		textNode.scale(.003f);

		node.attachChild(textNode);
		texts.add(textNode);
		Quaternion q = new Quaternion();
		q.lookAt(cam.getLocation(), cam.getUp());
		textNode.setLocalRotation(q);

	}

	private boolean isUnderGrid(int side, Tile tile) {
		return switch (side) {
		case 0, 2:
			yield Math.abs(tile.getPosition().y) < gridSize;
		case 1, 3:
			yield Math.abs(tile.getPosition().x) < gridSize;
		default:
			yield false;
		};
	}

	private void setAssociation(ArrayList<Tile> tiles) {

		@SuppressWarnings("unchecked")
		ArrayList<Tile> tileCopies = (ArrayList<Tile>) tiles.clone();
		tiles.forEach((tile) -> tile.map(tileCopies));
	}

	private Quaternion getRotation(int rotation) {
		Quaternion roll = new Quaternion();
		switch (rotation) {
		case 90 -> roll.fromAngleAxis(FastMath.PI / 2, new Vector3f(0, 1, 0));
		case 180 -> roll.fromAngleAxis(FastMath.PI, new Vector3f(0, 1, 0));
		case 270 -> roll.fromAngleAxis(FastMath.PI + (FastMath.PI / 2), new Vector3f(0, 1, 0));
		}
		return roll;
	}

	private void putShape(Node n, Mesh shape, ColorRGBA color) {
		Geometry g = new Geometry("coordinate axis", shape);
		Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
		mat.getAdditionalRenderState().setWireframe(true);
		mat.getAdditionalRenderState().setLineWidth(4);
		mat.setColor("Color", color);
		g.setMaterial(mat);
		n.attachChild(g);
	}

	private void attachCoordinateAxes(Node n) {
		Arrow arrow = new Arrow(Vector3f.UNIT_X);
		putShape(n, arrow, ColorRGBA.Red);

		arrow = new Arrow(Vector3f.UNIT_Y);
		putShape(n, arrow, ColorRGBA.Green);

		arrow = new Arrow(Vector3f.UNIT_Z);
		putShape(n, arrow, ColorRGBA.Blue);
	}

	@Override
	public void simpleUpdate(float tpf) {
		/*
		 * texts.forEach((text)->{ Quaternion q = new Quaternion();
		 * q.lookAt(cam.getLocation(),cam.getUp()); text.setLocalRotation(q); });
		 */
	}

	@Override
	public void simpleRender(RenderManager rm) {
		// TODO: add render code
	}

	@Override
	public void simpleInitApp() {
		setDisplayStatView(false);
		guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");

		setUI();
		setBG();
		setLight();
		resetCam();
		rootNode.attachChild(GEOM_NODE);

	}

	private void setLight() {
		AmbientLight ambient = new AmbientLight();
		ambient.setColor(ColorRGBA.White);
		rootNode.addLight(ambient);

		DirectionalLight sun = new DirectionalLight();
		sun.setDirection((new Vector3f(-0.5f, -0.5f, -0.5f)).normalizeLocal());
		sun.setColor(ColorRGBA.White);
		rootNode.addLight(sun);

	}

	private void setBG() {
		viewPort.setBackgroundColor(ColorRGBA.Blue);
	}

	private void setUI() {
		GuiGlobals.initialize(this);
		BaseStyles.loadGlassStyle();
		GuiGlobals.getInstance().getStyles().setDefaultStyle("glass");
		Container ui = new Container();
		guiNode.attachChild(ui);
		ui.addChild(new Label("Control"));
		
		Container pan = new Container(new BorderLayout());
		pan.addChild(new Label("Set Grid Size: "), BorderLayout.Position.West);
		gridSizeSetting = pan.addChild(new TextField(Integer.toString(gridSize)), BorderLayout.Position.Center);
		ui.addChild(pan);
		Button loadTiles = ui.addChild(new Button("Load Tiles"));
		Button renderSampleTiles = ui.addChild(new Button("Plot Tiles"));
		Button renderPattern = ui.addChild(new Button("Generate Pattern"));
		Button clear = ui.addChild(new Button("Clear"));
		Button resetCam = ui.addChild(new Button("Reset Camera"));
		clear.addClickCommands((c) -> clear());
		loadTiles.addClickCommands((c) -> loadTiles());
		renderSampleTiles.addClickCommands((c) -> plotTiles());
		renderPattern.addClickCommands((c) -> new Thread(()->renderPattern()).start());
	//	renderPattern.addClickCommands((c) -> renderPattern());
		resetCam.addClickCommands((c) -> resetCam());
		int w = settings.getWidth();
		int h = settings.getHeight();
		Vector3f size = ui.getPreferredSize();
		w = (int) (w - size.x);
		ui.setLocalTranslation(w, h, 0);

	}

	private void resetCam() {
		// Camera Position: (0.0, 3.1202602, 1.451486)
		// Camera Rotation: (0.0015113321, 0.81680024, -0.57691467, 0.0021397257)
		// Camera Direction: (0.0017516376, -0.9424546, -0.33432972)
		cam.setLocation(new Vector3f(0.0f, 3.1202602f, 1.451486f));
		cam.setRotation(new Quaternion(0.0015113321f, 0.81680024f, -0.57691467f, 0.0021397257f));

	}
}

You cant update the scene graph from any thread but the main thread. You can prepare updates (i.e do calculations or produce new non attached spatials) in another thread then apply them on the main thread.

What are you trying to achieve?

1 Like

like @richtea said above.

I will just add that if you want “synchronize” scene graph changes with JME render Thread, you can use:

app.enqueue(() -> {
       //scene graph changes here - app = JME app
});

not sure why your code is “out of JME loop scheme” but probably because of some “Thread.sleep(500);” or other things i dont see.

The other approach to accomplish adding nodes periodically if you don’t want to use separate threads is to just count time in update.

float time += tpf;
if( time > 5 ) {
    time = 0;
    do the thing
}
1 Like

This is for fun… I am trying to plot tiles with wave function algo. This is basic setup. once it is done I want to make it with more complex rule set.

I tried to create video but it is giving me this error.

Code:

	public static void main(String[] args) throws IOException {
		File video = File.createTempFile("WFC", ".avi");
		
		Main app = new Main();
		app.setTimer(new IsoTimer(60));
		app.setShowSettings(false);
		
		app.fullscreen(new AppSettings(true));
		Capture.SimpleCaptureVideo(app, video);
		app.start();
		app.restart();

	}

Error:

Exception in thread "main" java.lang.IncompatibleClassChangeError: Found interface com.jme3.app.Application, but class was expected
	at com.aurellem.capture.Capture.SimpleCaptureVideo(Unknown Source)
	at com.wfc.Main.main(Main.java:66)

Are you suggesting me to add nodes in simpleUpdate(float tpf) function?

I’m not sure exactly what you are asking. I was trying to answer this question.

…and I don’t know exactly what part of my answer you didn’t understand.

Finally I am able to capture video…
https://youtu.be/J3vqsHyDDFE

float time += tpf;
if( time > 5 ) {
time = 0;
do the thing
}

What I mean to aske is. Where should I put this code in my code? the tpf is received in simpleUpdate(float tpf) function. So, I guess you are suggesting to add node in simpleUpdate function.

Or in a control. Or in an app state. Or…

You asked how to do it not from a thread. “Somewhere in some update method” is the answer for how to do it not from another thread.

Whether that’s a good answer or not depends entirely on your end goal.

No not really, the update function could be used as a signaling mechanism for executing events, the event could be other thread waiting for this signal either polling the value (not recommended in general) or looping until receiving the value and you could also use thread locks or monitors (but try not to block the update thread as far as you can), it depends on your use case.

But, your original problem was updating the scene from another thread, if you have another problem, you can open another thread and ask the new question.

I was thinking the same.

The original problem is still open. See below code snippet. This is recursive method where I am adding node one by one as and when they are created. I wanted to show a node once it is created and take a pause for a moment before creating new one . To achieve the same I used thread, but turns out this is not good option. It is not throwing error as of now when videorecorder appstate is attached. I am not sure why.


	private void addNeighbours(Point position) {

		Map<Integer, Point> openPositions = grid.findOpenPositions(position);
		List<Tile> tmp = new LinkedList<>();
		for (Map.Entry<Integer, Point> positions : openPositions.entrySet()) {
			int side = positions.getKey();
			if (!isUnderGrid(side, positions.getValue())) {
				continue;
			}
			Map<Integer, Tile> neighbours = grid.findNeighbours(positions.getValue());
			Set<Tile> connections = new HashSet<>(4);
			neighbours.forEach((oppositSide, _tile) -> {
				if (connections.isEmpty()) {
					connections.addAll(_tile.getConnections(_tile.getOppositIndex(oppositSide)));
				} else {
					connections.retainAll(_tile.getConnections(_tile.getOppositIndex(oppositSide)));
				}
			});

			if (!connections.isEmpty()) {
				Tile n = new Tile(connections.toArray(new Tile[connections.size()])[dice.nextInt(connections.size())]);
				n.getPosition().setLocation(positions.getValue());
				grid.add(n);
				loadNode(n);
				tmp.add(n);
				enqueue(() -> {
					transformNode(n);
					GEOM_NODE.attachChild(n.getNode());
				});
				try {
					Thread.sleep(250);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		
		tmp.forEach((t) -> addNeighbours(t.getPosition()));
	}

By this do you mean it is now throwing error? If so what is the new error?

This is not a good reason to use threads, threads in games are meant for parallel-heavy stuff (asset loading, external hardware processing, image/sound/audio processing), I think the best simple answer would be Paul’s suggestion of utilizing the renderer time-per-frame (tpf) in a state or control, long-story-short, if all that you want is direct scene-graph manipulation then the game update should be the logic, however, you still can pre-transform an object on another thread before adding it to the scene graph, but once added, you cannot manipulate them from anything other than the game thread.

Yes, I am getting it also loading the same model every time is also not a good idea. I also understood what Paul’s suggestion is but my tiny brain is still trying to figure it out how to use and fit tfp logic into my code.

The call or the signal should be guarded with a time factor or boolean flag.

no, I mean when videorecorder appstate is attached. Its not throwing error. If I dont attach videorecorder appstate, it throws error as I mentioned first in this conversation.

The tpf is the time-per-frame, each frame rendered using the GPU takes some time depending on GPU Vram, speed of clocks, vsync and hsync, and other factors, while the Thread sleep relies on CPU clock cycles, choose wisely depending on your use case, if you are doing something that is tied to the CPU (controlling external inputs, asset loading,…) you should obviously utilize CPU only, you can still continue with your approach and signal the update thread to do something (like attaching the nodes or transforming them) after some CPU thread sleep.

Ah, then i think something is fundamentally wrong both ways but the videorecorder state just happens to cause a thread to run slower which means you just happen not to mutate the scenegraph at a particular time

Still not sure how to make it happen in code…