Files
narya/src/java/com/threerings/jme/tools/ModelViewer.java
T
Andrzej Kapolka 69c20acd43 Brought things up to date with the latest JME code.
git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4162 542714f4-19e9-0310-aa3c-eee0fc999fb1
2006-05-31 02:21:04 +00:00

718 lines
25 KiB
Java

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.jme.tools;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import javax.swing.KeyStroke;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileFilter;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.light.DirectionalLight;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Line;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.state.LightState;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.WireframeState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.JmeException;
import com.jme.util.LoggingSystem;
import com.jme.util.TextureManager;
import com.jme.util.geom.Debugger;
import com.samskivert.swing.GroupLayout;
import com.samskivert.swing.Spacer;
import com.samskivert.util.Config;
import com.threerings.resource.ResourceManager;
import com.threerings.util.MessageBundle;
import com.threerings.util.MessageManager;
import com.threerings.jme.JmeCanvasApp;
import com.threerings.jme.Log;
import com.threerings.jme.model.Model;
import com.threerings.jme.model.TextureProvider;
/**
* A simple viewer application that allows users to examine models and their
* animations by loading them from their uncompiled <code>.properties</code> /
* <code>.xml</code> representations or their compiled <code>.dat</code>
* representations.
*/
public class ModelViewer extends JmeCanvasApp
{
public static void main (String[] args)
{
// write standard output and error to a log file
if (System.getProperty("no_log_redir") == null) {
String dlog = "viewer.log";
try {
PrintStream logOut = new PrintStream(
new FileOutputStream(dlog), true);
System.setOut(logOut);
System.setErr(logOut);
} catch (IOException ioe) {
Log.warning("Failed to open debug log [path=" + dlog +
", error=" + ioe + "].");
}
}
LoggingSystem.getLoggingSystem().setLevel(Level.WARNING);
new ModelViewer(args.length > 0 ? args[0] : null);
}
/**
* Creates and initializes an instance of the model viewer application.
*
* @param path the path of the model to view, or <code>null</code> for
* none
*/
public ModelViewer (String path)
{
super(1024, 768);
_rsrcmgr = new ResourceManager("rsrc");
_msg = new MessageManager("rsrc.i18n").getBundle("jme.viewer");
_path = path;
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
_frame = new JFrame(_msg.get("m.title"));
_frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar menu = new JMenuBar();
_frame.setJMenuBar(menu);
JMenu file = new JMenu(_msg.get("m.file_menu"));
file.setMnemonic(KeyEvent.VK_F);
menu.add(file);
Action load = new AbstractAction(_msg.get("m.file_load")) {
public void actionPerformed (ActionEvent e) {
showLoadDialog();
}
};
load.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_L);
load.putValue(Action.ACCELERATOR_KEY,
KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_MASK));
file.add(load);
file.addSeparator();
Action quit = new AbstractAction(_msg.get("m.file_quit")) {
public void actionPerformed (ActionEvent e) {
System.exit(0);
}
};
quit.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_Q);
quit.putValue(Action.ACCELERATOR_KEY,
KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK));
file.add(quit);
JMenu view = new JMenu(_msg.get("m.view_menu"));
view.setMnemonic(KeyEvent.VK_V);
menu.add(view);
Action wireframe = new AbstractAction(_msg.get("m.view_wireframe")) {
public void actionPerformed (ActionEvent e) {
_wfstate.setEnabled(!_wfstate.isEnabled());
}
};
wireframe.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_W);
wireframe.putValue(Action.ACCELERATOR_KEY,
KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.CTRL_MASK));
view.add(new JCheckBoxMenuItem(wireframe));
view.addSeparator();
_pivots = new JCheckBoxMenuItem(_msg.get("m.view_pivots"));
_pivots.setMnemonic(KeyEvent.VK_P);
_pivots.setAccelerator(
KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_MASK));
view.add(_pivots);
_bounds = new JCheckBoxMenuItem(_msg.get("m.view_bounds"));
_bounds.setMnemonic(KeyEvent.VK_B);
_bounds.setAccelerator(
KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_MASK));
view.add(_bounds);
_normals = new JCheckBoxMenuItem(_msg.get("m.view_normals"));
_normals.setMnemonic(KeyEvent.VK_N);
_normals.setAccelerator(
KeyStroke.getKeyStroke(KeyEvent.VK_N, KeyEvent.CTRL_MASK));
view.add(_normals);
view.addSeparator();
Action rlight = new AbstractAction(_msg.get("m.view_light")) {
public void actionPerformed (ActionEvent e) {
if (_rldialog == null) {
_rldialog = new RotateLightDialog();
_rldialog.pack();
}
_rldialog.setVisible(true);
}
};
rlight.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
rlight.putValue(Action.ACCELERATOR_KEY,
KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_MASK));
view.add(new JMenuItem(rlight));
_frame.getContentPane().add(getCanvas(), BorderLayout.CENTER);
JPanel bpanel = new JPanel(new BorderLayout());
_frame.getContentPane().add(bpanel, BorderLayout.SOUTH);
_animctrls = new JPanel();
_animctrls.setBorder(BorderFactory.createEtchedBorder());
bpanel.add(_animctrls, BorderLayout.NORTH);
_animctrls.add(new JLabel(_msg.get("m.anim_select")));
_animctrls.add(_animbox = new JComboBox());
_animctrls.add(new JButton(
new AbstractAction(_msg.get("m.anim_start")) {
public void actionPerformed (ActionEvent e) {
_model.startAnimation((String)_animbox.getSelectedItem());
}
}));
_animctrls.add(_animstop = new JButton(
new AbstractAction(_msg.get("m.anim_stop")) {
public void actionPerformed (ActionEvent e) {
_model.stopAnimation();
}
}));
_animstop.setEnabled(false);
_animctrls.add(new Spacer(50, 1));
_animctrls.add(new JLabel(_msg.get("m.anim_speed")));
_animctrls.add(_animspeed = new JSlider(-100, +100, 0));
_animspeed.setMajorTickSpacing(10);
_animspeed.setSnapToTicks(true);
_animspeed.setPaintTicks(true);
_animspeed.addChangeListener(new ChangeListener() {
public void stateChanged (ChangeEvent e) {
updateAnimationSpeed();
}
});
_animctrls.setVisible(false);
_status = new JLabel(" ");
_status.setHorizontalAlignment(JLabel.LEFT);
_status.setBorder(BorderFactory.createEtchedBorder());
bpanel.add(_status, BorderLayout.SOUTH);
_frame.pack();
_frame.setVisible(true);
run();
}
@Override // documentation inherited
public boolean init ()
{
if (!super.init()) {
return false;
}
if (_path != null) {
loadModel(new File(_path));
}
return true;
}
@Override // documentation inherited
protected void initDisplay ()
throws JmeException
{
super.initDisplay();
_ctx.getRenderer().setBackgroundColor(ColorRGBA.gray);
}
@Override // documentation inherited
protected void initInput ()
{
super.initInput();
_camhand.setTiltLimits(FastMath.PI / 16.0f, FastMath.PI * 7.0f / 16.0f);
_camhand.setZoomLimits(1f, 100f);
_camhand.tiltCamera(-FastMath.PI * 7.0f / 16.0f);
MouseOrbiter orbiter = new MouseOrbiter();
_canvas.addMouseListener(orbiter);
_canvas.addMouseMotionListener(orbiter);
_canvas.addMouseWheelListener(orbiter);
}
@Override // documentation inherited
protected void initRoot ()
{
super.initRoot();
// set default states
MaterialState mstate = _ctx.getRenderer().createMaterialState();
mstate.getDiffuse().set(ColorRGBA.white);
mstate.getAmbient().set(ColorRGBA.white);
_ctx.getGeometry().setRenderState(mstate);
_ctx.getGeometry().setRenderState(
_wfstate = _ctx.getRenderer().createWireframeState());
_wfstate.setEnabled(false);
// create a grid on the XY plane to provide some reference
Vector3f[] points = new Vector3f[GRID_SIZE*2 + GRID_SIZE*2];
float halfLength = (GRID_SIZE - 1) * GRID_SPACING / 2;
int idx = 0;
for (int xx = 0; xx < GRID_SIZE; xx++) {
points[idx++] = new Vector3f(
-halfLength + xx*GRID_SPACING, -halfLength, 0f);
points[idx++] = new Vector3f(
-halfLength + xx*GRID_SPACING, +halfLength, 0f);
}
for (int yy = 0; yy < GRID_SIZE; yy++) {
points[idx++] = new Vector3f(
-halfLength, -halfLength + yy*GRID_SPACING, 0f);
points[idx++] = new Vector3f(
+halfLength, -halfLength + yy*GRID_SPACING, 0f);
}
Line grid = new Line("grid", points, null, null, null);
grid.getBatch(0).getDefaultColor().set(0.25f, 0.25f, 0.25f, 1f);
grid.setLightCombineMode(LightState.OFF);
grid.setModelBound(new BoundingBox());
grid.updateModelBound();
_ctx.getGeometry().attachChild(grid);
grid.updateRenderState();
// attach a dummy node to draw debugging views
_ctx.getGeometry().attachChild(new Node("debug") {
public void onDraw (Renderer r) {
if (_model == null) {
return;
}
if (_pivots.getState()) {
drawPivots(_model, r);
}
if (_bounds.getState()) {
Debugger.drawBounds(_model, r);
}
if (_normals.getState()) {
Debugger.drawNormals(_model, r, 5f, true);
}
}
});
}
/**
* Draws the pivot axes of the given node and its children.
*/
protected void drawPivots (Spatial spatial, Renderer r)
{
if (_axes == null) {
_axes = new Line("axes",
new Vector3f[] { Vector3f.ZERO, Vector3f.UNIT_X, Vector3f.ZERO,
Vector3f.UNIT_Y, Vector3f.ZERO, Vector3f.UNIT_Z }, null,
new ColorRGBA[] { ColorRGBA.red, ColorRGBA.red,
ColorRGBA.green, ColorRGBA.green, ColorRGBA.blue,
ColorRGBA.blue }, null);
_axes.setRenderState(r.createZBufferState());
}
_axes.getRenderState(ZBufferState.RS_ZBUFFER).apply();
_axes.getWorldTranslation().set(spatial.getWorldTranslation());
_axes.getWorldRotation().set(spatial.getWorldRotation());
_axes.draw(r);
if (spatial instanceof Node) {
Node node = (Node)spatial;
for (int ii = 0, nn = node.getQuantity(); ii < nn; ii++) {
drawPivots(node.getChild(ii), r);
}
}
}
@Override // documentation inherited
protected void initLighting ()
{
_dlight = new DirectionalLight();
_dlight.setEnabled(true);
_dlight.getDirection().set(-1f, 0f, -1f).normalizeLocal();
_dlight.getAmbient().set(0.25f, 0.25f, 0.25f, 1f);
LightState lstate = _ctx.getRenderer().createLightState();
lstate.attach(_dlight);
_ctx.getGeometry().setRenderState(lstate);
_ctx.getGeometry().setLightCombineMode(LightState.REPLACE);
}
/**
* Shows the load model dialog.
*/
protected void showLoadDialog ()
{
if (_chooser == null) {
_chooser = new JFileChooser();
_chooser.setDialogTitle(_msg.get("m.load_title"));
_chooser.setFileFilter(new FileFilter() {
public boolean accept (File file) {
if (file.isDirectory()) {
return true;
}
String path = file.toString();
return path.endsWith(".properties") ||
path.endsWith(".dat");
}
public String getDescription () {
return _msg.get("m.load_filter");
}
});
File dir = new File(_config.getValue("dir", "."));
if (dir.exists()) {
_chooser.setCurrentDirectory(dir);
}
}
if (_chooser.showOpenDialog(_frame) == JFileChooser.APPROVE_OPTION) {
loadModel(_chooser.getSelectedFile());
}
_config.setValue("dir", _chooser.getCurrentDirectory().toString());
}
/**
* Attempts to load a model from the specified location.
*/
protected void loadModel (File file)
{
String fpath = file.toString();
try {
if (fpath.endsWith(".properties")) {
compileModel(file);
} else if (fpath.endsWith(".dat")) {
loadCompiledModel(file);
} else {
throw new Exception(_msg.get("m.invalid_type"));
}
_status.setText(_msg.get("m.loaded_model", fpath));
} catch (Exception e) {
e.printStackTrace();
_status.setText(_msg.get("m.load_error", fpath, e));
}
}
/**
* Attempts to compile and load a model.
*/
protected void compileModel (File file)
throws Exception
{
_status.setText(_msg.get("m.compiling_model", file));
Model model = CompileModel.compile(file);
if (model != null) {
setModel(model, file);
return;
}
// if compileModel returned null, the .dat file is up-to-date
String fpath = file.toString();
int didx = fpath.lastIndexOf('.');
fpath = (didx == -1) ? fpath : fpath.substring(0, didx);
loadCompiledModel(new File(fpath + ".dat"));
}
/**
* Attempts to load a model that has already been compiled.
*/
protected void loadCompiledModel (File file)
throws IOException
{
_status.setText(_msg.get("m.loading_model", file));
setModel(Model.readFromFile(file, false), file);
}
/**
* Sets the model once it's been loaded.
*
* @param file the file from which the model was loaded
*/
protected void setModel (Model model, File file)
{
if (_model != null) {
_ctx.getGeometry().detachChild(_model);
}
_ctx.getGeometry().attachChild(_model = model);
_model.lockStaticMeshes(_ctx.getRenderer(), true, true);
// resolve the textures from the file's directory
final File dir = file.getParentFile();
_model.resolveTextures(new TextureProvider() {
public TextureState getTexture (String name) {
TextureState tstate = _tstates.get(name);
if (tstate == null) {
File file;
if (name.startsWith("/")) {
file = _rsrcmgr.getResourceFile(name.substring(1));
} else {
file = new File(dir, name);
}
Texture tex = TextureManager.loadTexture(file.toString(),
Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR);
if (tex == null) {
Log.warning("Couldn't find texture [path=" + file +
"].");
return null;
}
tex.setWrap(Texture.WM_WRAP_S_WRAP_T);
tstate = _ctx.getRenderer().createTextureState();
tstate.setTexture(tex);
_tstates.put(name, tstate);
}
return tstate;
}
protected HashMap<String, TextureState> _tstates =
new HashMap<String, TextureState>();
});
_model.updateRenderState();
// configure the animation panel
String[] anims = _model.getAnimationNames();
if (anims.length == 0) {
_animctrls.setVisible(false);
return;
}
_model.addAnimationObserver(_animobs);
_animctrls.setVisible(true);
_animbox.setModel(new DefaultComboBoxModel(anims));
updateAnimationSpeed();
}
/**
* Updates the model's animation speed based on the position of the
* animation speed slider.
*/
protected void updateAnimationSpeed ()
{
_model.setAnimationSpeed(
FastMath.pow(2f, _animspeed.getValue() / 25f));
}
/** The resource manager. */
protected ResourceManager _rsrcmgr;
/** The translation bundle. */
protected MessageBundle _msg;
/** The path of the initial model to load. */
protected String _path;
/** The viewer frame. */
protected JFrame _frame;
/** Debug view switches. */
protected JCheckBoxMenuItem _pivots, _bounds, _normals;
/** The animation controls. */
protected JPanel _animctrls;
/** The animation selector. */
protected JComboBox _animbox;
/** The "stop animation" button. */
protected JButton _animstop;
/** The animation speed slider. */
protected JSlider _animspeed;
/** The status bar. */
protected JLabel _status;
/** The model file chooser. */
protected JFileChooser _chooser;
/** The light rotation dialog. */
protected RotateLightDialog _rldialog;
/** The scene light. */
protected DirectionalLight _dlight;
/** Used to toggle wireframe rendering. */
protected WireframeState _wfstate;
/** The currently loaded model. */
protected Model _model;
/** Reused to draw pivot axes. */
protected Line _axes;
/** Enables and disables the stop button when animations start and stop. */
protected Model.AnimationObserver _animobs =
new Model.AnimationObserver() {
public boolean animationStarted (Model model, String name) {
_animstop.setEnabled(true);
return true;
}
public boolean animationCompleted (Model model, String name) {
_animstop.setEnabled(false);
return true;
}
public boolean animationCancelled (Model model, String name) {
_animstop.setEnabled(false);
return true;
}
};
/** Allows users to move the directional light around. */
protected class RotateLightDialog extends JDialog
implements ChangeListener
{
public RotateLightDialog ()
{
super(_frame, _msg.get("m.rotate_light"), false);
JPanel cpanel = GroupLayout.makeVBox();
getContentPane().add(cpanel, BorderLayout.CENTER);
JPanel apanel = new JPanel();
apanel.add(new JLabel(_msg.get("m.azimuth")));
apanel.add(_azimuth = new JSlider(-180, +180, 0));
_azimuth.addChangeListener(this);
cpanel.add(apanel);
JPanel epanel = new JPanel();
epanel.add(new JLabel(_msg.get("m.elevation")));
epanel.add(_elevation = new JSlider(-90, +90, 45));
_elevation.addChangeListener(this);
cpanel.add(epanel);
JPanel bpanel = new JPanel();
bpanel.add(new JButton(new AbstractAction(_msg.get("m.close")) {
public void actionPerformed (ActionEvent e) {
setVisible(false);
}
}));
getContentPane().add(bpanel, BorderLayout.SOUTH);
}
// documentation inherited from interface ChangeListener
public void stateChanged (ChangeEvent e)
{
float az = _azimuth.getValue() * FastMath.DEG_TO_RAD,
el = _elevation.getValue() * FastMath.DEG_TO_RAD;
_dlight.getDirection().set(
-FastMath.cos(az) * FastMath.cos(el),
-FastMath.sin(az) * FastMath.cos(el),
-FastMath.sin(el));
}
// documentation inherited from interface ActionListener
public void actionPerformed (ActionEvent e)
{
setVisible(false);
}
/** Azimuth and elevation sliders. */
protected JSlider _azimuth, _elevation;
}
/** Moves the camera using mouse input. */
protected class MouseOrbiter extends MouseAdapter
implements MouseMotionListener, MouseWheelListener
{
@Override // documentation inherited
public void mousePressed (MouseEvent e)
{
_mloc.setLocation(e.getX(), e.getY());
}
// documentation inherited from interface MouseMotionListener
public void mouseMoved (MouseEvent e)
{
}
// documentation inherited from interface MouseMotionListener
public void mouseDragged (MouseEvent e)
{
int dx = e.getX() - _mloc.x, dy = e.getY() - _mloc.y;
_mloc.setLocation(e.getX(), e.getY());
if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
_camhand.tiltCamera(dy * FastMath.PI / 1000);
_camhand.orbitCamera(-dx * FastMath.PI / 1000);
} else {
_camhand.zoomCamera(dy);
}
}
// documentation inherited from interface MouseWheelListener
public void mouseWheelMoved (MouseWheelEvent e)
{
_camhand.zoomCamera(e.getWheelRotation() * 10f);
}
/** The last recorded position of the mouse cursor. */
protected Point _mloc = new Point();
}
/** The app configuration. */
protected static Config _config =
new Config("com/threerings/jme/tools/ModelViewer");
/** The number of lines on the grid in each direction. */
protected static final int GRID_SIZE = 32;
/** The spacing between lines on the grid. */
protected static final float GRID_SPACING = 2.5f;
}