[TOOL] Node Tree Viewer

[Scene debugging] [Node children debugging]

As I encountered an mystically ever increasing triangle count as I logged out and in into my game, I had to find the reason as to why this was happening.
No visual oddities could be seen ingame so I decided to debug the rootNode content. I remembered from before some code that would dump the contents into
a html-file as a list, but this time it seemed too simple and wouldnt make it simple enough to debug.

So I ended up writing up a simple JFrame containing a JTree that would continuously update, with the ability to pause to browse.
Either way I found this to be useful and I assumed I could throw it up here for anyone needing to do the same in the future.

The code is pretty straightforward, all you need to do is create the object and pass in the application rootNode and call its update method in the update method of the application.

[java]
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

import java.awt.EventQueue;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;

public class NodeTreeViewer extends javax.swing.JFrame {

/**
 * 
 */
private static final long serialVersionUID = 8855945690857269184L;
private final Node rootNode;
private final DefaultMutableTreeNode top = new DefaultMutableTreeNode(
		"Scene");
private final DefaultTreeModel model = new DefaultTreeModel(top);

private boolean update = true;
private long lastUpdate = 0;
// Frequency of updates
private final static long UPDATE_SLEEP = 1000;

/**
 * Creates new form NodeTreeViewer
 */
public NodeTreeViewer(final Node rootNode) {
	this.rootNode = rootNode;
	initComponents();
	update();
	setTitle("Node Tree Viewer");
	setVisible(true);
}

final Runnable updateTask = new Runnable() {
	@Override
	public void run() {
		top.removeAllChildren();
		addSpatial(top, rootNode);
		model.reload();
	}
};

public void update() {
	if (!update) {
		return;
	}
	if ((System.currentTimeMillis() - lastUpdate) > UPDATE_SLEEP) {
		lastUpdate = System.currentTimeMillis();
		EventQueue.invokeLater(updateTask);
	}
}

private void addSpatial(final DefaultMutableTreeNode treeParent,
		final Spatial spatial) {
	final DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(
			spatial.getName());
	if (spatial instanceof Node) {
		for (final Spatial n : ((Node) spatial).getChildren()) {
			addSpatial(treeNode, n);
		}
	}
	treeParent.add(treeNode);
}

/**
 * This method is called from within the constructor to initialize the form.
 * WARNING: Do NOT modify this code. The content of this method is always
 * regenerated by the Form Editor.
 */
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed"
// <editor-fold defaultstate="collapsed"
// <editor-fold defaultstate="collapsed"
// //GEN-BEGIN:initComponents
private void initComponents() {

	mainPanel = new javax.swing.JPanel();
	scrollPane = new javax.swing.JScrollPane();
	nodeTree = new javax.swing.JTree(model);
	pauseToggle = new javax.swing.JToggleButton();

	setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

	scrollPane.setViewportView(nodeTree);

	pauseToggle.setText("Pause");
	pauseToggle.addActionListener(new java.awt.event.ActionListener() {
		@Override
		public void actionPerformed(final java.awt.event.ActionEvent evt) {
			pauseToggleActionPerformed(evt);
		}
	});

	final javax.swing.GroupLayout mainPanelLayout = new javax.swing.GroupLayout(
			mainPanel);
	mainPanel.setLayout(mainPanelLayout);
	mainPanelLayout.setHorizontalGroup(mainPanelLayout
			.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
			.addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE,
					759, Short.MAX_VALUE)
			.addComponent(pauseToggle,
					javax.swing.GroupLayout.DEFAULT_SIZE,
					javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE));
	mainPanelLayout
			.setVerticalGroup(mainPanelLayout
					.createParallelGroup(
							javax.swing.GroupLayout.Alignment.LEADING)
					.addGroup(
							mainPanelLayout
									.createSequentialGroup()
									.addComponent(scrollPane)
									.addPreferredGap(
											javax.swing.LayoutStyle.ComponentPlacement.RELATED)
									.addComponent(pauseToggle)));

	final javax.swing.GroupLayout layout = new javax.swing.GroupLayout(
			getContentPane());
	getContentPane().setLayout(layout);
	layout.setHorizontalGroup(layout.createParallelGroup(
			javax.swing.GroupLayout.Alignment.LEADING).addComponent(
			mainPanel, javax.swing.GroupLayout.DEFAULT_SIZE,
			javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE));
	layout.setVerticalGroup(layout.createParallelGroup(
			javax.swing.GroupLayout.Alignment.LEADING).addComponent(
			mainPanel, javax.swing.GroupLayout.DEFAULT_SIZE,
			javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE));

	pack();
}// //GEN-END:initComponents

private void pauseToggleActionPerformed(final java.awt.event.ActionEvent evt) {// GEN-FIRST:event_pauseToggleActionPerformed
	if (pauseToggle.isSelected()) {
		pauseToggle.setText("Allow updates");
		update = false;
	} else {
		pauseToggle.setText("Pause");
		update = true;
	}
}// GEN-LAST:event_pauseToggleActionPerformed

// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JPanel mainPanel;
private javax.swing.JTree nodeTree;
private javax.swing.JToggleButton pauseToggle;
private javax.swing.JScrollPane scrollPane;
// End of variables declaration//GEN-END:variables

}
[/java]

Netbeans visual builder form (for the layout)
Paste2: NodeTreeViewer.form

5 Likes

Cool, thanks for sharing though I think I should mention its not threadsafe, you have to call swing UI modifications from the EDT thread. This is very likely to crash or freeze on certain OSs and JVMs.

The SDK allows you to look at the contents of your AppStates like this in the SceneExplorer now, in the current RC2 you could save your scene to j3o (see wiki) and open that to look at it :slight_smile:

Thanks !

The number of times I had walked the rootNode using JDK debug windows ! This will definitely save my time !

Not wishing to take away from the original post (which is far more impressive) but I have this code triggered by a debug keypress in my application, It is a similar idea although not as sophisticated (but also lightweight enough to always leave in place).

I can use it to dump the scene graph to the log whenever I need to, comes in handy sometimes.

[java]
public static void printSceneGraph() {
Node node = AppContext.getJme3().getRootNode();
StringBuilder graph = new StringBuilder("\n");
buildGraph(node, graph, “”);
logger.log(Level.INFO, graph.toString());
}

private static void buildGraph(Spatial parent, StringBuilder graph, String indent) {
    graph.append(indent).append(parent).append(parent.getLocalTranslation()).append("\n");
    if (parent instanceof Node) {
        indent += "\t";
        for (Spatial s: ((Node)parent).getChildren())
            buildGraph(s, graph, indent);
    }
}
[/java]
1 Like

@perfecticus dont you mind if i add your example to simpleExamples project?

Here: http://code.google.com/p/jme-simple-examples/

I also can add you as a comitter here.

@mift: As said, its really a bad example of swing threading, I guess it should definitely be extended to be included there.

@normen said: @mift: As said, its really a bad example of swing threading, I guess it should definitely be extended to be included there.

Aaahh… sorry… ok, i will wait for correct example. :slight_smile:

@normen said: @mift: As said, its really a bad example of swing threading, I guess it should definitely be extended to be included there.

Hmm, the intentions of the updateTask is to run it on the EDT, which part of it is not threadsafe?

The code in the update method will check if its running from the EDT, if not, it will call the invoke method in the EventQueue,
this should make the swing modifications run in the EDT.

I assume I should’ve used a non blocking call of it using invokeLater instead though.

But please explain, which part of it isnt thread safe?

[java]
public void update() {
if (!update) {
return;
}
if (EventQueue.isDispatchThread()) {
updateTask.run();
} else {
try {
EventQueue.invokeAndWait(updateTask);
} catch (final InvocationTargetException e) {
e.printStackTrace();
} catch (final InterruptedException e) {
e.printStackTrace();
}
}
}
[/java]

@mifth Oh, yes its fine to include it in that project.

Right, the issue isn’t that you don’t dispatch on the EDT, thats only what I looked for when I found that you block the EDT against the update loop, lwjgl doesn’t like that in some situations on some platforms. Sorry, I really have to sleep more ^^

Fair enough :stuck_out_tongue:

Updated the code to not use blocking, and added a frequency of updates var.
Also added a link to the netbeans gui builder form.

For some reason it was marked as spam though,
@normen if you care to unmark it

What the… and now your top post is just… gone? Though I unspammed it? Maybe I accidentally marked it as spam that, I don’t remember misclicking the “spam” link though… Sorry about that, no idea what happened but it wasn’t intentional :frowning:

Edit: re-saved the post via the wp admin panel, seems like that worked?

Never mind, already answered :slight_smile:

Yea the post looks like the last edited one, either way kinda strange :stuck_out_tongue:

@normen said: I don't remember misclicking the "spam" link though... Sorry about that, no idea what happened but it wasn't intentional :(
Sleep dude.....really ;)

@perfecticus , i have added your cool tool to the simpleExamp0les project. Thank you!

http://code.google.com/p/jme-simple-examples/source/detail?r=ad0408aac57357bda2e3b58b2d437c1c28137dbf