Skip to content

Instantly share code, notes, and snippets.

@NetzwergX
Last active May 18, 2020 06:59
Show Gist options
  • Save NetzwergX/81c4f8afa6077679a409f62f8826a1a2 to your computer and use it in GitHub Desktop.
Save NetzwergX/81c4f8afa6077679a409f62f8826a1a2 to your computer and use it in GitHub Desktop.
Scene graph information display for JME3. Provides an app state that shows which geometry is picked, its parents & their controls and allows toggling wireframe on/off for picked geometry.
/*
* Copyright (c) 2018 Sebastian Teumert (<http://teumert.net>)
* Some rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.teumert.jme3.app.state.debug;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.material.Material;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.Spatial.CullHint;
/**
* <p>Debug app state to allow introspection of the scene graph by displaying the picked geometry,
* all its parents and optionally their controls, as well as toggling wireframe state on picked geometries.</p>
*
* <p><b>Usage</b></p>
* <p>
* The app state has sensible defaults and can be used as-is, without configuration.
* Simply add it to the <code>StateManager</code> via <code>stateManager.attach(new SceneGraphDebugAppState())</code>
* and it works out-of-the box.
* </p>
* <p>
* See {@link SceneGraphDebugAppState#SceneGraphDebugAppState(PickMode)} for default configurations.
* </p>
* <p><b>Configuration</b></p>
* <p>
* If the default configuration is not sufficient, the behavior can be overriden. There are several constructors
* that aid in configuraing the proper triggers. Note that if you are using anything else then the default constructor,
* <b>only</b> the triggers explicitly specified will be added, and all other trggers will not be set, disabling that
* functionality.
* </p>
* <p><b>Example #1</b></p>
* <p>If you want only the scene graph element stack, but not the wireframe toggling capability, you
* may use the {@link SceneGraphDebugAppState#SceneGraphDebugAppState(PickMode, Trigger, Trigger)} constructor.
* </p>
* <p><b>Example #2</b></p>
* <p>If you want only the scene graph element stack, with control information always on, you
* may use the {@link SceneGraphDebugAppState#SceneGraphDebugAppState(PickMode, Trigger)} constructor, followed by
* <code>setShowControls(true)</code>.
* </p>
* <p>
* The location, alignment and grow direction of the scene stack display can be configured via
* {@link SceneGraphDebugAppState#SceneGraphDebugAppState(Vector3f, Alignment, GrowDirecton)}.
* For example, moving the display to the top-left corner, making it grow down and aligned to the left of the screen may
* be done
* </p>
* <p><b>Important</b></p>
* <p>
* Per default, picking is done on the scene first attached to the main viewport of the app. You can change the used
* scene by calling {@link SceneGraphDebugAppState#setScene(Spatial}. Picking in the whole scene might be very
* expensive. Choose proper subsecene to allow for better picking performance, especially when using bigger scenes!
* </p>
* <p>
* Picking in multiple scenes is not supported. Create multiple instances of the app state and provide them with
* the various scenes you wish to inspect.
* </p>
*
* @author Sebastian Teumert
* @since JME3.2.0-stable
* @date 2018-07-20
* @version 1.0.0
*/
public class SceneGraphDebugAppState extends BaseAppState implements ActionListener {
/**
* Specifies the picking behavior.
* @author Sebastian Teumert
*
*/
public static enum PickMode {
/**
* <p>
* In <code>Mode.CameraDirection</code>, the picked geometry will always be at the center of the screen.<br/>
* Use it if you are having a first-person camera or another camera with disabled mouse,
* e.g. <code>FlyBycamera</code>.
* </p>
*/
CameraDirection,
/**
* <p>
* In <code>Mode.MousePosition</code>, he picked geometr will always be below the mouse cursor.<br/>
* It is only sensible to use this when the mouse cursor is enabled.
* </p>
*/
MousePosition
}
/**
* Specifies the text alignment
* @author Sebastian Teumert
*
*/
public static enum Alignment {
/**
* Left-aligned text
*/
Left,
/**
* Right-aligned text
*/
Right
}
/**
* Specifies the grow direction of the scene graph stack.
* @author Sebastian Teumert
*
*/
public static enum GrowDirection {
/**
* Most specific geometry will be at the bottom, parent elements will be added above.
*/
Up,
/**
* Most specific geomatry will be at the top, parent elements will be added below.
*/
Down
}
private AssetManager assetManager;
private InputManager inputManager;
private PickMode mode = PickMode.MousePosition;
private Trigger
toggleDisplay,
toggleControls,
toggleWireframe,
resetWireframe;
private Camera camera;
private Spatial scene;
private BitmapFont font;
private Node display;
private boolean showControls = false;
private Alignment alignment = Alignment.Right;
private GrowDirection growDirection = GrowDirection.Up;
private Vector3f position;
private Map<Material, Boolean> isWireframeMap;
private SceneGraphDebugAppState() {
display = new Node("GUI/SceneStack");
// set proper state for GUI
display.setQueueBucket(Bucket.Gui);
display.setCullHint(CullHint.Never);
this.isWireframeMap = new HashMap<Material, Boolean>();
}
/**
* <p>Creates a default state with the given mode, with the display placed in the lower right corner of the screen,
* aligned to thr right of the screen and growing upwards.</p>
* <p>
* In <code>Mode.CameraDirection</code>, the picked geometry will always be at the center of the screen.<br/>
* Use it if you are having a first-person camera or another camera with disabled mouse,
* e.g. <code>FlyBycamera</code>.
* </p>
* <p>
* In <code>Mode.MousePosition</code>, he picked geometr will always be below the mouse cursor.<br/>
* It is only sensible to use this when the mouse cursor is enabled.
* </p>
* <p>
* Triggers will be
* <ul>
* <li><b>F5</b> to toggle the display on/off</li>
* <li><b>F6</b> to toggle display of attached controls on/off</li>
* <li><b>F7</b> to toggle wireframe of the picked geometry on/off</li>
* <li><b>F8</b> to reset all wireframed geometries back to solid</li>
* </ul>
* </p>
* @param mode The picking mode
*/
public SceneGraphDebugAppState (PickMode mode) {
this(mode, new KeyTrigger(KeyInput.KEY_F5), new KeyTrigger(KeyInput.KEY_F6),
new KeyTrigger(KeyInput.KEY_F7), new KeyTrigger(KeyInput.KEY_F8));
}
/**
* Creates an state with the given mode and trigger for toggling the display.
* @param mode Picking mode
* @param toggleDisplay Trigger for toggling the display on/off. May be null to disable.
*/
public SceneGraphDebugAppState (PickMode mode, Trigger toggleDisplay) {
this();
this.mode = mode;
this.toggleDisplay = toggleDisplay;
}
/**
* Creates an state with the given mode and trigger for toggling the display and control information.
* @param mode Picking mode
* @param toggleDisplay Trigger for toggling the display on/off. May be null to disable.
* @param toggleControls Trigger for toggling of control information. May be null to disable.
*/
public SceneGraphDebugAppState (PickMode mode, Trigger toggleDisplay, Trigger toggleControls) {
this(mode, toggleDisplay);
this.toggleControls = toggleControls;
}
/**
* Creates an state with the given mode and trigger for toggling the display and control information, as well as
* enabling/disabling wireframe.
* @param mode Picking mode
* @param toggleDisplay Trigger for toggling the display on/off. May be null to disable.
* @param toggleControls Trigger for toggling of control information. May be null to disable.
* @param toggleWireframe Trigger for toggling wireframe of the selected geometry. May be null to disable.
*/
public SceneGraphDebugAppState (PickMode mode, Trigger toggleDisplay, Trigger toggleControls, Trigger toggleWireframe) {
this(mode, toggleDisplay, toggleControls);
this.toggleWireframe = toggleWireframe;
}
/**
* Creates an state with the given mode and trigger for toggling the display and control information, as well as
* enabling/disabling wireframe and resetting wireframe to solid on all changed geometries.
* @param mode Picking mode
* @param toggleDisplay Trigger for toggling the display on/off. May be null to disable.
* @param toggleControls Trigger for toggling of control information. May be null to disable.
* @param toggleWireframe Trigger for toggling wireframe of the selected geometry. May be null to disable.
* @param resetWireframe Trigger for resetting all affected geometries back to solid. May be null to disable.
*/
public SceneGraphDebugAppState (PickMode mode, Trigger toggleDisplay, Trigger toggleControls,
Trigger toggleWireframe, Trigger resetWireframe) {
this(mode, toggleDisplay, toggleControls, toggleWireframe);
this.resetWireframe = resetWireframe;
}
@Override
protected void initialize (Application app) {
this.inputManager = app.getInputManager();
this.assetManager = app.getAssetManager();
if (toggleDisplay != null) {
inputManager.addMapping("SceneGraphDebugAppState#toggleDisplay", toggleDisplay);
inputManager.addListener(this, "SceneGraphDebugAppState#toggleDisplay");
}
if (toggleControls != null) {
inputManager.addMapping("SceneGraphDebugAppState#toggleControls", toggleControls);
inputManager.addListener(this, "SceneGraphDebugAppState#toggleControls");
}
if (toggleWireframe != null) {
inputManager.addMapping("SceneGraphDebugAppState#toggleWireframe", toggleWireframe);
inputManager.addListener(this, "SceneGraphDebugAppState#toggleWireframe");
}
if (resetWireframe != null) {
inputManager.addMapping("SceneGraphDebugAppState#resetWireframe", resetWireframe);
inputManager.addListener(this, "SceneGraphDebugAppState#resetWireframe");
}
this.camera = app.getCamera();
this.scene = app.getViewPort().getScenes().get(0);
font = assetManager.loadFont("Interface/Fonts/Console.fnt");
// font = assetManager.loadFont("Interface/Fonts/Default.fnt");
// lower right of the screen
if (position == null)
position = new Vector3f(camera.getWidth() - 10, 10, 0);
display.setLocalTranslation(position);
((Node)app.getGuiViewPort().getScenes().get(0)).attachChild(display);
}
@Override
protected void cleanup (Application app) {
inputManager.deleteMapping("SceneGraphDebugAppState#toggleDisplay");
inputManager.deleteMapping("SceneGraphDebugAppState#toggleControls");
inputManager.deleteMapping("SceneGraphDebugAppState#toggleWireframe");
inputManager.deleteMapping("SceneGraphDebugAppState#resetWireframe");
inputManager.removeListener(this);
}
@Override
public void update (float tpf) {
if (!isEnabled())
return;
display.detachAllChildren();
CollisionResult collision = doPicking();
if (collision == null)
return;
Stack<String> sceneStack = new Stack<String>();
Spatial current = collision.getGeometry();
do {
String controls = "";
if (showControls) {
controls += "{";
if(current.getNumControls() > 0)
controls += current.getControl(0).getClass().getSimpleName();
for (int i = 1; i < current.getNumControls() ; i++)
controls += ", " + current.getControl(i).getClass().getSimpleName();
controls += "}";
}
sceneStack.add(current.getName() + " [" + current.getClass().getSimpleName() + "]" + controls);
current = current.getParent();
}
while (current/*.getParent()*/ != null);
float height = 0f;
//TODO cache recently used elements - good cache size?
for (String element : sceneStack) {
BitmapText text = new BitmapText(font);
text.setText(element);
float xOffset;
if (growDirection == GrowDirection.Up)
height += ((int)(text.getLineHeight() * 1.2f));
else
height -= ((int)(text.getLineHeight() * 1.2f));
if (alignment == Alignment.Right)
xOffset = -text.getLineWidth();
else
xOffset = 0;
text.setLocalTranslation(xOffset, height, 0);
display.attachChild(text);
}
super.update(tpf);
}
@Override
protected void onEnable () {}
@Override
protected void onDisable () {
display.detachAllChildren();
}
@Override
public void onAction (String name, boolean isPressed, float tpf) {
// only work if the key is released (after the press)
if (name.equals("SceneGraphDebugAppState#toggleDisplay") && !isPressed)
// toggle this state
this.setEnabled(!this.isEnabled());
else if (name.equals("SceneGraphDebugAppState#toggleControls") && !isPressed)
this.showControls = !this.showControls;
else if (name.equals("SceneGraphDebugAppState#toggleWireframe") && !isPressed) {
CollisionResult collision = doPicking();
if (collision != null) {
Boolean isWf = isWireframeMap.getOrDefault(collision.getGeometry().getMaterial(), false);
isWireframeMap.put(collision.getGeometry().getMaterial(), !isWf);
collision.getGeometry().getMaterial().getAdditionalRenderState().setWireframe(!isWf);
}
}
else if (name.equals("SceneGraphDebugAppState#resetWireframe") && !isPressed) {
for (Material mat : isWireframeMap.keySet())
mat.getAdditionalRenderState().setWireframe(false);
isWireframeMap.clear();
}
}
/**
* The pick mode
* @return
*/
public PickMode getMode () {
return mode;
}
/**
* Sets the pick mode.
* @param mode
*/
public void setMode (PickMode mode) {
this.mode = mode;
}
/**
* The scene to analyse
* @return
*/
public Spatial getScene () {
return scene;
}
/**
* Sets the scene to analyse
* @param scene
*/
public void setScene (Spatial scene) {
this.scene = scene;
}
/**
* Whether additional control information is shown or not.
* @return
*/
public boolean isShowControls () {
return showControls;
}
/**
* Set if additional control information should be shown.
* @param showControls
*/
public void setShowControls (boolean showControls) {
this.showControls = showControls;
}
/**
* Sets the behavior of the information display
* @param position on-screen position of the information display, in GUI space
* @param alignment Left- or right aligment of the shown text
* @param direction grow direction of the display
*/
public void setPosition(Vector3f position, Alignment alignment, GrowDirection direction) {
display.setLocalTranslation(this.position = position);
this.alignment = alignment;
this.growDirection = direction;
}
private CollisionResult doPicking() {
Vector3f origin = new Vector3f();
Vector3f direction = new Vector3f();
if (mode == PickMode.CameraDirection) {
origin.set(camera.getLocation());
direction.set(camera.getDirection());
} else if (mode == PickMode.MousePosition) {
Vector2f click = inputManager.getCursorPosition().clone();
origin.set(camera.getWorldCoordinates(click, 0f));
direction.set(camera.getWorldCoordinates(click, 1f)
.subtractLocal(origin).normalizeLocal());
} else {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Illegal mode: " + mode);
}
Ray ray = new Ray(origin, direction);
CollisionResults results = new CollisionResults();
scene.collideWith(ray, results);
CollisionResult collision = results.getClosestCollision();
return collision;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment