diff --git a/build.xml b/build.xml index d701b9b2a..e0368558c 100644 --- a/build.xml +++ b/build.xml @@ -243,6 +243,7 @@ + diff --git a/rsrc/i18n/jme/viewer.properties b/rsrc/i18n/jme/viewer.properties new file mode 100644 index 000000000..06093f2be --- /dev/null +++ b/rsrc/i18n/jme/viewer.properties @@ -0,0 +1,19 @@ +# +# $Id$ +# +# Translation strings for ModelViewer messages. + +m.title = Three Rings Model Viewer + +m.file_menu = File +m.file_load = Load Model... +m.file_quit = Quit + +m.load_title = Select Model to Load +m.load_filter = Model Files (*.properties, *.dat) + +m.compiling_model = Compiling {0}... +m.loading_model = Loading {0}... +m.loaded_model = Loaded {0} +m.load_error = Error loading {0}: {1} +m.invalid_type = Invalid file type. diff --git a/src/java/com/threerings/jme/camera/CameraHandler.java b/src/java/com/threerings/jme/camera/CameraHandler.java index a9d84e72d..b59c0698e 100644 --- a/src/java/com/threerings/jme/camera/CameraHandler.java +++ b/src/java/com/threerings/jme/camera/CameraHandler.java @@ -89,13 +89,13 @@ public class CameraHandler } /** - * Configures the minimum and maximum z-axis elevation allowed for the + * Configures the minimum and maximum zoom values allowed for the * camera. */ - public void setZoomLimits (float minZ, float maxZ) + public void setZoomLimits (float minZoom, float maxZoom) { - _minZ = minZ; - _maxZ = maxZ; + _minZoom = minZoom; + _maxZoom = maxZoom; } /** @@ -230,6 +230,12 @@ public class CameraHandler public void zoomCamera (float distance) { Vector3f loc = _camera.getLocation(); + float dist = -1f * _ground.normal.dot(loc) / + _ground.normal.dot(_camera.getDirection()), + ndist = Math.min(Math.max(dist + distance, _minZoom), _maxZoom); + if ((distance = ndist - dist) == 0f) { + return; + } loc.subtractLocal(_camera.getDirection().mult(distance, _temp)); setLocation(loc); } @@ -247,6 +253,17 @@ public class CameraHandler // which we're going to orbit Vector3f direction = _camera.getLocation().subtract(spot); + // if we're rotating around the left vector, impose tilt limits + if (axis == _camera.getLeft()) { + float angle = FastMath.asin(_ground.normal.dot(direction) / + direction.length()); + float nangle = Math.min(Math.max(angle + deltaAngle, _minTilt), + _maxTilt); + if ((deltaAngle = nangle - angle) == 0f) { + return; + } + } + // create a rotation matrix _rotm.fromAxisAngle(axis, deltaAngle); @@ -383,6 +400,7 @@ public class CameraHandler protected float _minX = -Float.MAX_VALUE, _maxX = Float.MAX_VALUE; protected float _minY = -Float.MAX_VALUE, _maxY = Float.MAX_VALUE; protected float _minZ = -Float.MAX_VALUE, _maxZ = Float.MAX_VALUE; + protected float _minZoom = 0f, _maxZoom = Float.MAX_VALUE; protected float _minTilt = -Float.MAX_VALUE, _maxTilt = Float.MAX_VALUE; protected Vector3f _rxdir = new Vector3f(1, 0, 0); diff --git a/src/java/com/threerings/jme/model/BoneNode.java b/src/java/com/threerings/jme/model/BoneNode.java index f83c6580e..f6c0c93cd 100644 --- a/src/java/com/threerings/jme/model/BoneNode.java +++ b/src/java/com/threerings/jme/model/BoneNode.java @@ -21,7 +21,6 @@ package com.threerings.jme.model; -import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; diff --git a/src/java/com/threerings/jme/model/Model.java b/src/java/com/threerings/jme/model/Model.java index 51b9d53e4..fc4cccf22 100644 --- a/src/java/com/threerings/jme/model/Model.java +++ b/src/java/com/threerings/jme/model/Model.java @@ -30,8 +30,12 @@ import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; +import java.util.Properties; + import com.threerings.jme.Log; /** @@ -63,8 +67,10 @@ public class Model extends ModelNode FileChannel fc = fis.getChannel(); if (map) { long pos = fc.position(); - model.sliceBuffers(fc.map(FileChannel.MapMode.READ_ONLY, - pos, fc.size() - pos)); + MappedByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, + pos, fc.size() - pos); + buf.order(ByteOrder.LITTLE_ENDIAN); + model.sliceBuffers(buf); } else { model.readBuffers(fc); } @@ -82,9 +88,18 @@ public class Model extends ModelNode /** * Standard constructor. */ - public Model (String name) + public Model (String name, Properties props) { super(name); + _props = props; + } + + /** + * Returns a reference to the properties of the model. + */ + public Properties getProperties () + { + return _props; } /** @@ -109,14 +124,19 @@ public class Model extends ModelNode throws IOException { super.writeExternal(out); + out.writeObject(_props); } @Override // documentation inherited public void readExternal (ObjectInput in) - throws IOException + throws IOException, ClassNotFoundException { super.readExternal(in); + _props = (Properties)in.readObject(); } + /** The model properties. */ + protected Properties _props; + private static final long serialVersionUID = 1; } diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java index 12a5d5e9c..fc3eefdee 100644 --- a/src/java/com/threerings/jme/model/ModelMesh.java +++ b/src/java/com/threerings/jme/model/ModelMesh.java @@ -27,11 +27,16 @@ import java.io.ObjectInput; import java.io.ObjectOutput; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; +import java.util.Properties; + +import com.jme.bounding.BoundingBox; +import com.jme.bounding.BoundingSphere; import com.jme.math.Quaternion; import com.jme.math.Vector3f; import com.jme.scene.TriMesh; @@ -49,6 +54,7 @@ public class ModelMesh extends TriMesh */ public ModelMesh () { + super("mesh"); } /** @@ -69,6 +75,15 @@ public class ModelMesh extends TriMesh super(name, vertices, normals, colors, tcoords, indices); } + /** + * Configures this mesh based on the given (sub-)properties. + */ + public void configure (Properties props) + { + _boundingType = "sphere".equals(props.getProperty("bound")) ? + SPHERE_BOUND : BOX_BOUND; + } + /** * Sets the buffers as {@link ByteBuffer}s, because we can't create byte * views of non-byte buffers. @@ -88,6 +103,13 @@ public class ModelMesh extends TriMesh _colorByteBuffer = colors; _textureByteBuffer = textures; _indexByteBuffer = indices; + + if (_boundingType == BOX_BOUND) { + setModelBound(new BoundingBox()); + } else { // _boundingType == SPHERE_BOUND + setModelBound(new BoundingSphere()); + } + updateModelBound(); } // documentation inherited @@ -130,27 +152,23 @@ public class ModelMesh extends TriMesh out.writeInt(_colorBufferSize); out.writeInt(_textureBufferSize); out.writeInt(_indexBufferSize); + out.writeInt(_boundingType); } // documentation inherited from interface Externalizable public void readExternal (ObjectInput in) - throws IOException + throws IOException, ClassNotFoundException { setName(in.readUTF()); - try { - setLocalTranslation((Vector3f)in.readObject()); - setLocalRotation((Quaternion)in.readObject()); - setLocalScale((Vector3f)in.readObject()); - - } catch (ClassNotFoundException e) { - Log.warning("Encounted unknown class [mesh=" + getName() + - ", exception=" + e + "]."); - } + setLocalTranslation((Vector3f)in.readObject()); + setLocalRotation((Quaternion)in.readObject()); + setLocalScale((Vector3f)in.readObject()); _vertexBufferSize = in.readInt(); _normalBufferSize = in.readInt(); _colorBufferSize = in.readInt(); _textureBufferSize = in.readInt(); _indexBufferSize = in.readInt(); + _boundingType = in.readInt(); } // documentation inherited from interface ModelSpatial @@ -159,23 +177,33 @@ public class ModelMesh extends TriMesh { if (_vertexBufferSize > 0) { _vertexByteBuffer.rewind(); + convertOrder(_vertexByteBuffer, ByteOrder.LITTLE_ENDIAN); out.write(_vertexByteBuffer); + convertOrder(_vertexByteBuffer, ByteOrder.nativeOrder()); } if (_normalBufferSize > 0) { _normalByteBuffer.rewind(); + convertOrder(_normalByteBuffer, ByteOrder.LITTLE_ENDIAN); out.write(_normalByteBuffer); + convertOrder(_normalByteBuffer, ByteOrder.nativeOrder()); } if (_colorBufferSize > 0) { _colorByteBuffer.rewind(); + convertOrder(_colorByteBuffer, ByteOrder.LITTLE_ENDIAN); out.write(_colorByteBuffer); + convertOrder(_colorByteBuffer, ByteOrder.nativeOrder()); } if (_textureBufferSize > 0) { - _indexByteBuffer.rewind(); - out.write(_indexByteBuffer); + _textureByteBuffer.rewind(); + convertOrder(_textureByteBuffer, ByteOrder.LITTLE_ENDIAN); + out.write(_textureByteBuffer); + convertOrder(_textureByteBuffer, ByteOrder.nativeOrder()); } if (_indexBufferSize > 0) { _indexByteBuffer.rewind(); + convertOrder(_indexByteBuffer, ByteOrder.LITTLE_ENDIAN); out.write(_indexByteBuffer); + convertOrder(_indexByteBuffer, ByteOrder.nativeOrder()); } } @@ -185,30 +213,36 @@ public class ModelMesh extends TriMesh { ByteBuffer vbbuf = null, nbbuf = null, cbbuf = null, tbbuf = null, ibbuf = null; + ByteOrder le = ByteOrder.LITTLE_ENDIAN; if (_vertexBufferSize > 0) { - vbbuf = ByteBuffer.allocateDirect(_vertexBufferSize*3*4); + vbbuf = ByteBuffer.allocateDirect(_vertexBufferSize*4).order(le); in.read(vbbuf); vbbuf.rewind(); + convertOrder(vbbuf, ByteOrder.nativeOrder()); } if (_normalBufferSize > 0) { - nbbuf = ByteBuffer.allocateDirect(_normalBufferSize*3*4); + nbbuf = ByteBuffer.allocateDirect(_normalBufferSize*4).order(le); in.read(nbbuf); nbbuf.rewind(); + convertOrder(nbbuf, ByteOrder.nativeOrder()); } if (_colorBufferSize > 0) { - cbbuf = ByteBuffer.allocateDirect(_colorBufferSize*4*4); + cbbuf = ByteBuffer.allocateDirect(_colorBufferSize*4).order(le); in.read(cbbuf); cbbuf.rewind(); + convertOrder(cbbuf, ByteOrder.nativeOrder()); } if (_textureBufferSize > 0) { - tbbuf = ByteBuffer.allocateDirect(_textureBufferSize*2*4); + tbbuf = ByteBuffer.allocateDirect(_textureBufferSize*4).order(le); in.read(tbbuf); tbbuf.rewind(); + convertOrder(tbbuf, ByteOrder.nativeOrder()); } if (_indexBufferSize > 0) { - ibbuf = ByteBuffer.allocateDirect(_indexBufferSize*4); + ibbuf = ByteBuffer.allocateDirect(_indexBufferSize*4).order(le); in.read(ibbuf); ibbuf.rewind(); + convertOrder(ibbuf, ByteOrder.nativeOrder()); } reconstruct(vbbuf, nbbuf, cbbuf, tbbuf, ibbuf); } @@ -218,26 +252,27 @@ public class ModelMesh extends TriMesh { ByteBuffer vbbuf = null, nbbuf = null, cbbuf = null, tbbuf = null, ibbuf = null; + int total = 0; if (_vertexBufferSize > 0) { - int npos = map.position() + _vertexBufferSize*3*4; + int npos = map.position() + _vertexBufferSize*4; map.limit(npos); vbbuf = map.slice(); map.position(npos); } if (_normalBufferSize > 0) { - int npos = map.position() + _normalBufferSize*3*4; + int npos = map.position() + _normalBufferSize*4; map.limit(npos); nbbuf = map.slice(); map.position(npos); } if (_colorBufferSize > 0) { - int npos = map.position() + _colorBufferSize*4*4; + int npos = map.position() + _colorBufferSize*4; map.limit(npos); cbbuf = map.slice(); map.position(npos); } if (_textureBufferSize > 0) { - int npos = map.position() + _textureBufferSize*2*4; + int npos = map.position() + _textureBufferSize*4; map.limit(npos); tbbuf = map.slice(); map.position(npos); @@ -251,6 +286,21 @@ public class ModelMesh extends TriMesh reconstruct(vbbuf, nbbuf, cbbuf, tbbuf, ibbuf); } + /** + * Imposes the specified order on the given buffer of 32 bit values. + */ + protected void convertOrder (ByteBuffer buf, ByteOrder order) + { + if (buf.order() == order) { + return; + } + IntBuffer obuf = buf.asIntBuffer(), + nbuf = buf.order(order).asIntBuffer(); + while (obuf.hasRemaining()) { + nbuf.put(obuf.get()); + } + } + /** The sizes of the various buffers (zero for null). */ protected int _vertexBufferSize, _normalBufferSize, _colorBufferSize, _textureBufferSize, _indexBufferSize; @@ -259,5 +309,14 @@ public class ModelMesh extends TriMesh protected ByteBuffer _vertexByteBuffer, _normalByteBuffer, _colorByteBuffer, _textureByteBuffer, _indexByteBuffer; + /** The type of bounding volume that this mesh should use. */ + protected int _boundingType; + + /** Indicates that this mesh should use a bounding box. */ + protected static final int BOX_BOUND = 0; + + /** Indicates that this mesh should use a bounding sphere. */ + protected static final int SPHERE_BOUND = 1; + private static final long serialVersionUID = 1; } diff --git a/src/java/com/threerings/jme/model/ModelNode.java b/src/java/com/threerings/jme/model/ModelNode.java index 56ff69233..c6fc60b56 100644 --- a/src/java/com/threerings/jme/model/ModelNode.java +++ b/src/java/com/threerings/jme/model/ModelNode.java @@ -50,6 +50,7 @@ public class ModelNode extends Node */ public ModelNode () { + super("node"); } /** @@ -86,23 +87,18 @@ public class ModelNode extends Node // documentation inherited from interface Externalizable public void readExternal (ObjectInput in) - throws IOException + throws IOException, ClassNotFoundException { setName(in.readUTF()); - try { - setLocalTranslation((Vector3f)in.readObject()); - setLocalRotation((Quaternion)in.readObject()); - setLocalScale((Vector3f)in.readObject()); - ArrayList children = (ArrayList)in.readObject(); - for (int ii = 0, nn = children.size(); ii < nn; ii++) { - attachChild((Spatial)children.get(ii)); - } - } catch (ClassNotFoundException e) { - Log.warning("Encounted unknown class [node=" + getName() + - ", exception=" + e + "]."); + setLocalTranslation((Vector3f)in.readObject()); + setLocalRotation((Quaternion)in.readObject()); + setLocalScale((Vector3f)in.readObject()); + ArrayList children = (ArrayList)in.readObject(); + for (int ii = 0, nn = children.size(); ii < nn; ii++) { + attachChild((Spatial)children.get(ii)); } } - + // documentation inherited from interface ModelSpatial public void writeBuffers (FileChannel out) throws IOException diff --git a/src/java/com/threerings/jme/model/SkinMesh.java b/src/java/com/threerings/jme/model/SkinMesh.java index 44cc6fbe2..2f914836b 100644 --- a/src/java/com/threerings/jme/model/SkinMesh.java +++ b/src/java/com/threerings/jme/model/SkinMesh.java @@ -21,7 +21,6 @@ package com.threerings.jme.model; -import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; @@ -108,21 +107,19 @@ public class SkinMesh extends ModelMesh @Override // documentation inherited public void readExternal (ObjectInput in) - throws IOException + throws IOException, ClassNotFoundException { super.readExternal(in); - try { - _weightGroups = (WeightGroup[])in.readObject(); - } catch (ClassNotFoundException e) { - Log.warning("Encounted unknown class [mesh=" + getName() + - ", exception=" + e + "]."); - } + _weightGroups = (WeightGroup[])in.readObject(); } @Override // documentation inherited public void updateWorldData (float time) { super.updateWorldData(time); + if (_weightGroups == null) { + return; + } // deform the mesh according to the positions of the bones for (int ii = 0; ii < _weightGroups.length; ii++) { diff --git a/src/java/com/threerings/jme/tools/CompileModelTask.java b/src/java/com/threerings/jme/tools/CompileModelTask.java index ff213dc50..302d30127 100644 --- a/src/java/com/threerings/jme/tools/CompileModelTask.java +++ b/src/java/com/threerings/jme/tools/CompileModelTask.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.ObjectOutputStream; import java.util.ArrayList; +import java.util.Properties; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; @@ -47,6 +48,42 @@ import com.threerings.jme.tools.xml.ModelParser; */ public class CompileModelTask extends Task { + /** + * Loads the model described by the given properties file and compiles it + * to a .dat file in the same directory. + * + * @return the loaded model, or null if the compiled version + * is up-to-date + */ + public static Model compileModel (File source) + throws Exception + { + String spath = source.toString(); + int didx = spath.lastIndexOf('.'); + String root = (didx == -1) ? spath : spath.substring(0, didx); + File content = new File(root + ".xml"), + target = new File(root + ".dat"); + if (source.lastModified() < target.lastModified() && + content.lastModified() < target.lastModified()) { + return null; + } + System.out.println("Compiling " + source.getParent() + "..."); + + // load the model properties + Properties props = new Properties(); + FileInputStream in = new FileInputStream(source); + props.load(in); + in.close(); + + // load the model content + ModelDef mdef = _mparser.parseModel(content.toString()); + Model model = mdef.createModel(props); + + // write and return the model + model.writeToFile(target); + return model; + } + public void addFileset (FileSet set) { _filesets.add(set); @@ -62,45 +99,19 @@ public class CompileModelTask extends Task String[] srcFiles = ds.getIncludedFiles(); for (int f = 0; f < srcFiles.length; f++) { - File cfile = new File(fromDir, srcFiles[f]); - String target = srcFiles[f]; - int didx = target.lastIndexOf("."); - target = (didx == -1) ? target : target.substring(0, didx); - target += ".dat"; - compileModel(cfile, new File(fromDir, target)); + File source = new File(fromDir, srcFiles[f]); + try { + compileModel(source); + } catch (Exception e) { + System.err.println("Error compiling " + source + ": " + e); + } } } } - - protected void compileModel (File source, File target) - { - if (source.lastModified() < target.lastModified()) { - return; - } - System.out.println("Compiling " + source + "..."); - - ModelDef mdef; - try { - mdef = _mparser.parseModel(source.toString()); - - } catch (Exception e) { - System.err.println("Error parsing '" + source + "': " + e); - return; - } - - Model model = mdef.createModel("model"); - try { - FileOutputStream fos = new FileOutputStream(target); - new ObjectOutputStream(fos).writeObject(model); - - } catch (IOException e) { - System.err.println("Error writing '" + target + "': " + e); - } - } - + /** A list of filesets that contain XML models. */ protected ArrayList _filesets = new ArrayList(); /** A parser for the model definitions. */ - protected ModelParser _mparser = new ModelParser(); + protected static ModelParser _mparser = new ModelParser(); } diff --git a/src/java/com/threerings/jme/tools/ModelDef.java b/src/java/com/threerings/jme/tools/ModelDef.java index ac89c7b6c..93d99f46c 100644 --- a/src/java/com/threerings/jme/tools/ModelDef.java +++ b/src/java/com/threerings/jme/tools/ModelDef.java @@ -22,6 +22,7 @@ package com.threerings.jme.tools; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; @@ -30,10 +31,14 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Properties; +import com.jme.math.FastMath; import com.jme.scene.Spatial; import com.jme.util.geom.BufferUtils; +import com.samskivert.util.PropertiesUtil; + import com.threerings.jme.Log; import com.threerings.jme.model.BoneNode; import com.threerings.jme.model.Model; @@ -61,10 +66,10 @@ public class ModelDef public float[] scale; /** Returns a JME node for this definition. */ - public Spatial getSpatial () + public Spatial getSpatial (Properties props) { if (_spatial == null) { - _spatial = createSpatial(); + _spatial = createSpatial(props); setTransform(); } return _spatial; @@ -81,7 +86,7 @@ public class ModelDef } /** Creates a JME node for this definition. */ - public abstract Spatial createSpatial (); + public abstract Spatial createSpatial (Properties props); /** Resolves any name references using the supplied map. */ public void resolveReferences (HashMap nodes) @@ -124,19 +129,25 @@ public class ModelDef } // documentation inherited - public Spatial createSpatial () + public Spatial createSpatial (Properties props) { - return setBuffers(new ModelMesh(name)); + return configure(new ModelMesh(name), props); } - /** Sets the mesh's geometry buffers and returns it. */ - protected ModelMesh setBuffers (ModelMesh mmesh) + /** Configures the new mesh and returns it. */ + protected ModelMesh configure (ModelMesh mmesh, Properties props) { + // configure using sub-properties + mmesh.configure(PropertiesUtil.getSubProperties(props, name, "")); + + // set the various buffers int vsize = vertices.size(); - ByteBuffer vbbuf = ByteBuffer.allocateDirect(vsize*3*4), - nbbuf = ByteBuffer.allocateDirect(vsize*3*4), - tbbuf = tcoords ? ByteBuffer.allocateDirect(vsize*2*4) : null, - ibbuf = ByteBuffer.allocateDirect(indices.size()*4); + ByteOrder no = ByteOrder.nativeOrder(); + ByteBuffer vbbuf = ByteBuffer.allocateDirect(vsize*3*4).order(no), + nbbuf = ByteBuffer.allocateDirect(vsize*3*4).order(no), + tbbuf = tcoords ? + ByteBuffer.allocateDirect(vsize*2*4).order(no) : null, + ibbuf = ByteBuffer.allocateDirect(indices.size()*4).order(no); FloatBuffer vbuf = vbbuf.asFloatBuffer(), nbuf = nbbuf.asFloatBuffer(), tbuf = tcoords ? tbbuf.asFloatBuffer() : null; @@ -156,9 +167,9 @@ public class ModelDef public static class SkinMeshDef extends TriMeshDef { // documentation inherited - public Spatial createSpatial () + public Spatial createSpatial (Properties props) { - return setBuffers(new SkinMesh(name)); + return configure(new SkinMesh(name), props); } @Override // documentation inherited @@ -167,17 +178,17 @@ public class ModelDef super.resolveReferences(nodes); // divide the vertices up by weight groups - HashMap, WeightGroupDef> groups = - new HashMap, WeightGroupDef>(); + HashMap, WeightGroupDef> groups = + new HashMap, WeightGroupDef>(); for (int ii = 0, nn = vertices.size(); ii < nn; ii++) { SkinVertex svertex = (SkinVertex)vertices.get(ii); - HashSet bones = svertex.getBoneNames(); + HashSet bones = svertex.getBonesAndNormalize(nodes); WeightGroupDef group = groups.get(bones); if (group == null) { groups.put(bones, group = new WeightGroupDef()); } group.indices.add(ii); - for (String bone : bones) { + for (BoneNode bone : bones) { group.weights.add(svertex.getWeight(bone)); } } @@ -186,21 +197,12 @@ public class ModelDef SkinMesh.WeightGroup[] wgroups = new SkinMesh.WeightGroup[groups.size()]; int ii = 0; - for (Map.Entry, WeightGroupDef> entry : + for (Map.Entry, WeightGroupDef> entry : groups.entrySet()) { SkinMesh.WeightGroup wgroup = new SkinMesh.WeightGroup(); wgroup.indices = toArray(entry.getValue().indices); - wgroup.bones = new BoneNode[entry.getKey().size()]; - int jj = 0; - for (String bone : entry.getKey()) { - Spatial node = nodes.get(bone); - if (!(node instanceof BoneNode)) { - Log.warning("Missing or invalid bone reference " + - "[mesh=" + name + ", bone=" + bone + "]."); - return; - } - wgroup.bones[jj++] = (BoneNode)node; - } + HashSet bones = entry.getKey(); + wgroup.bones = bones.toArray(new BoneNode[bones.size()]); wgroup.weights = toArray(entry.getValue().weights); wgroups[ii++] = wgroup; } @@ -212,7 +214,7 @@ public class ModelDef public static class NodeDef extends SpatialDef { // documentation inherited - public Spatial createSpatial () + public Spatial createSpatial (Properties props) { return new ModelNode(name); } @@ -222,7 +224,7 @@ public class ModelDef public static class BoneNodeDef extends NodeDef { // documentation inherited - public Spatial createSpatial () + public Spatial createSpatial (Properties props) { return new BoneNode(name); } @@ -239,7 +241,17 @@ public class ModelDef FloatBuffer vbuf, FloatBuffer nbuf, FloatBuffer tbuf) { vbuf.put(location); + + // make sure the normal is normalized + float nlen = FastMath.sqrt(normal[0]*normal[0] + + normal[1]*normal[1] + normal[2]*normal[2]); + if (nlen != 1f) { + normal[0] /= nlen; + normal[1] /= nlen; + normal[2] /= nlen; + } nbuf.put(normal); + if (tbuf != null) { tbuf.put(tcoords); } @@ -262,24 +274,39 @@ public class ModelDef public void addBoneWeight (BoneWeight weight) { - boneWeights.add(weight); - } - - /** Returns the set of names of the bones influencing this vertex. */ - public HashSet getBoneNames () - { - HashSet names = new HashSet(); - for (BoneWeight bweight : boneWeights) { - names.add(bweight.bone); + if (weight.weight > 0f) { + boneWeights.add(weight); } - return names; } - /** Returns the weight of the named bone. */ - public float getWeight (String bone) + /** Finds the bone nodes influencing this vertex and normalizes the + * weights. */ + public HashSet getBonesAndNormalize ( + HashMap nodes) { + HashSet bones = new HashSet(); + float totalWeight = 0f; for (BoneWeight bweight : boneWeights) { - if (bweight.bone.equals(bone)) { + Spatial node = nodes.get(bweight.bone); + if (node instanceof BoneNode) { + bones.add((BoneNode)node); + totalWeight += bweight.weight; + } + } + if (totalWeight > 0f && totalWeight < 1f) { + for (BoneWeight bweight : boneWeights) { + bweight.weight /= totalWeight; + } + } + return bones; + } + + /** Returns the weight of the given bone. */ + public float getWeight (BoneNode bone) + { + String name = bone.getName(); + for (BoneWeight bweight : boneWeights) { + if (bweight.bone.equals(name)) { return bweight.weight; } } @@ -318,14 +345,14 @@ public class ModelDef /** * Creates the model node defined herein. */ - public Model createModel (String name) + public Model createModel (Properties props) { - Model model = new Model(name); + Model model = new Model(props.getProperty("name", "model"), props); // start by creating the spatials and mapping them to their names HashMap nodes = new HashMap(); for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { - Spatial spatial = spatials.get(ii).getSpatial(); + Spatial spatial = spatials.get(ii).getSpatial(props); nodes.put(spatial.getName(), spatial); } @@ -334,12 +361,12 @@ public class ModelDef for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { SpatialDef sdef = spatials.get(ii); sdef.resolveReferences(nodes); - if (sdef.getSpatial().getParent() == null) { - model.attachChild(sdef.getSpatial()); + if (sdef.getSpatial(props).getParent() == null) { + model.attachChild(sdef.getSpatial(props)); } } - return model; + return model; } /** Converts a boxed Integer list to an unboxed int array. */ diff --git a/src/java/com/threerings/jme/tools/ModelViewer.java b/src/java/com/threerings/jme/tools/ModelViewer.java new file mode 100644 index 000000000..c38836284 --- /dev/null +++ b/src/java/com/threerings/jme/tools/ModelViewer.java @@ -0,0 +1,387 @@ +// +// $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.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.IOException; + +import java.util.logging.Level; + +import javax.swing.AbstractAction; +import javax.swing.Action; +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.KeyStroke; +import javax.swing.filechooser.FileFilter; + +import com.jme.light.DirectionalLight; +import com.jme.math.FastMath; +import com.jme.math.Vector3f; +import com.jme.renderer.ColorRGBA; +import com.jme.scene.Line; +import com.jme.scene.state.LightState; +import com.jme.scene.state.MaterialState; +import com.jme.util.LoggingSystem; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.util.Config; + +import com.threerings.util.MessageBundle; +import com.threerings.util.MessageManager; + +import com.threerings.jme.JmeCanvasApp; +import com.threerings.jme.model.Model; + +/** + * A simple viewer application that allows users to examine models and their + * animations by loading them from their uncompiled .properties / + * .xml representations or their compiled .dat + * representations. + */ +public class ModelViewer extends JmeCanvasApp +{ + public static void main (String[] args) + { + 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 null for + * none + */ + public ModelViewer (String path) + { + super(1024, 768); + _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")); + 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); + + _frame.getContentPane().add(getCanvas(), BorderLayout.CENTER); + + _controls = GroupLayout.makeVBox(GroupLayout.NONE, GroupLayout.TOP); + _controls.setPreferredSize(new Dimension(100, 100)); + _frame.getContentPane().add(_controls, BorderLayout.EAST); + + _status = new JLabel(" "); + _status.setHorizontalAlignment(JLabel.LEFT); + _frame.getContentPane().add(_status, BorderLayout.SOUTH); + + _frame.pack(); + _frame.setVisible(true); + + LoggingSystem.getLoggingSystem().setLevel(Level.WARNING); + + 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 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 a default material + MaterialState mstate = _ctx.getRenderer().createMaterialState(); + mstate.getDiffuse().set(ColorRGBA.white); + mstate.getAmbient().set(ColorRGBA.white); + _ctx.getGeometry().setRenderState(mstate); + + // 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.getDefaultColor().set(0.25f, 0.25f, 0.25f, 1f); + grid.setLightCombineMode(LightState.OFF); + _ctx.getGeometry().attachChild(grid); + grid.updateRenderState(); + } + + @Override // documentation inherited + protected void initLighting () + { + DirectionalLight 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 = CompileModelTask.compileModel(file); + if (model != null) { + setModel(model); + 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)); + } + + /** + * Sets the model once it's been loaded. + */ + protected void setModel (Model model) + { + if (_model != null) { + _ctx.getGeometry().detachChild(_model); + } + _ctx.getGeometry().attachChild(_model = model); + _model.setLocalScale(0.04f); + _model.updateRenderState(); + } + + /** The translation bundle. */ + protected MessageBundle _msg; + + /** The path of the initial model to load. */ + protected String _path; + + /** The viewer frame. */ + protected JFrame _frame; + + /** The control panel. */ + protected JPanel _controls; + + /** The status bar. */ + protected JLabel _status; + + /** The model file chooser. */ + protected JFileChooser _chooser; + + /** The currently loaded model. */ + protected Model _model; + + /** 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; +} diff --git a/src/java/com/threerings/jme/tools/xml/ModelParser.java b/src/java/com/threerings/jme/tools/xml/ModelParser.java index a73964f17..460e592b1 100644 --- a/src/java/com/threerings/jme/tools/xml/ModelParser.java +++ b/src/java/com/threerings/jme/tools/xml/ModelParser.java @@ -71,21 +71,23 @@ public class ModelParser _digester.addSetNext(bnode, "addSpatial", ModelDef.SpatialDef.class.getName()); - String vertex = tmesh + "/vertex"; + String vertex = tmesh + "/vertex", svertex = smesh + "/vertex"; _digester.addObjectCreate(vertex, ModelDef.Vertex.class.getName()); - _digester.addObjectCreate(smesh + "/vertex", + _digester.addObjectCreate(svertex, ModelDef.SkinVertex.class.getName()); - _digester.addRule("*/vertex", new SetPropertyFieldsRule()); - + _digester.addRule(vertex, new SetPropertyFieldsRule()); + _digester.addRule(svertex, new SetPropertyFieldsRule()); + _digester.addSetNext(vertex, "addVertex", + ModelDef.Vertex.class.getName()); + _digester.addSetNext(svertex, "addVertex", + ModelDef.Vertex.class.getName()); + String bweight = smesh + "/vertex/boneWeight"; _digester.addObjectCreate(bweight, ModelDef.BoneWeight.class.getName()); _digester.addRule(bweight, new SetPropertyFieldsRule()); _digester.addSetNext(bweight, "addBoneWeight", ModelDef.BoneWeight.class.getName()); - - _digester.addSetNext("*/vertex", "addVertex", - ModelDef.Vertex.class.getName()); } /** diff --git a/src/ms/TRModelExporter.mcr b/src/ms/TRModelExporter.mcr index 07cc9b199..dc4abb0f9 100644 --- a/src/ms/TRModelExporter.mcr +++ b/src/ms/TRModelExporter.mcr @@ -68,7 +68,7 @@ macroScript TRModelExporter category:"File" \ if mesh.numtverts > 0 do ( tvface = getTVFace mesh i ) - for i = 1 to 3 do ( + for i = 1 to 3 do in coordsys local ( format " \n" to:outFile if isProperty node #skin do ( - modPanel.setCurrentObject node.skin node:node ui:true + setCommandPanelTaskMode mode:#modify + modPanel.setCurrentObject node.skin node:node ) writeVertices node outFile format " \n\n" kind to:outFile ) else ( - format "\>\n\n" to:outFile + format "/>\n\n" to:outFile ) )