From 488d47262bf13245d7d10a9e8b4f9799c145d955 Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Wed, 18 Apr 2007 21:06:08 +0000 Subject: [PATCH] Where possible, merge meshes with identical attributes. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@201 ed5b42cb-e716-0410-a449-f6a68f950b19 --- .../com/threerings/jme/model/ModelMesh.java | 85 +++++++- .../com/threerings/jme/tools/ModelDef.java | 188 ++++++++++++------ 2 files changed, 211 insertions(+), 62 deletions(-) diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java index e80b471f..31223a6f 100644 --- a/src/java/com/threerings/jme/model/ModelMesh.java +++ b/src/java/com/threerings/jme/model/ModelMesh.java @@ -29,6 +29,7 @@ import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Properties; import com.jme.bounding.BoundingBox; @@ -141,6 +142,51 @@ public class ModelMesh extends TriMesh Boolean.parseBoolean(tprops.getProperty("translucent")); } + /** + * Returns an opaque object representing this mesh's attributes. The + * object will implement {@link Object#hashCode} and {@link Object#equals} + * so that meshes with the same attributes will have identical keys. + */ + public Object getAttributeKey () + { + return new AttributeKey(_textureKey, _textures, _sphereMapped, + _filterMode, _mipMapMode, _emissiveMap, _emissive, _additive, + _solid, _transparent, _alphaThreshold, _translucent); + } + + /** + * Merges another mesh into this one. The mesh is assumed to be in the same coordinate system + * and have the same attributes. + */ + public void merge (ModelMesh mesh) + { + TriangleBatch batch = (TriangleBatch)getBatch(0); + TriangleBatch mbatch = (TriangleBatch)mesh.getBatch(0); + + int vcount = batch.getVertexCount(); + batch.setVertexBuffer(concatenate(batch.getVertexBuffer(), mbatch.getVertexBuffer())); + batch.setNormalBuffer(concatenate(batch.getNormalBuffer(), mbatch.getNormalBuffer())); + if (batch.getColorBuffer() != null && mbatch.getColorBuffer() != null) { + batch.setColorBuffer(concatenate(batch.getColorBuffer(), mbatch.getColorBuffer())); + } + batch.setTextureBuffer(concatenate( + batch.getTextureBuffer(0), mbatch.getTextureBuffer(0)), 0); + for (int ii = 1, nn = getTextureCount(); ii < nn; ii++) { + batch.setTextureBuffer(batch.getTextureBuffer(0), ii); + } + + IntBuffer ibuf = batch.getIndexBuffer(), mibuf = mbatch.getIndexBuffer(); + IntBuffer nibuf = BufferUtils.createIntBuffer(ibuf.capacity() + mibuf.capacity()); + ibuf.clear(); + nibuf.put(ibuf); + mibuf.clear(); + while (mibuf.hasRemaining()) { + nibuf.put(mibuf.get() + vcount); + } + nibuf.clear(); + batch.setIndexBuffer(nibuf); + } + /** * Adjusts the vertices and the transform of the mesh so that the mesh's * position lies at the center of its bounding volume. @@ -588,6 +634,20 @@ public class ModelMesh extends TriMesh return astate; } + /** + * Concatenates the two provided buffers. + */ + protected static FloatBuffer concatenate (FloatBuffer b1, FloatBuffer b2) + { + FloatBuffer nb = BufferUtils.createFloatBuffer(b1.capacity() + b2.capacity()); + b1.clear(); + nb.put(b1); + b2.clear(); + nb.put(b2); + nb.clear(); + return nb; + } + /** * Sorts the encoded triangle index/distance pairs in {@link #_tcodes} * using a two-pass (16 bit) radix sort (as described by @@ -745,8 +805,29 @@ public class ModelMesh extends TriMesh new RenderState[RenderState.RS_MAX_STATE]; } - /** The type of bounding volume that this mesh should use. */ - protected int _boundingType; + /** A wrapper around an array of attributes that uses {@link Arrays#deepHashCode} and + * {@link Arrays#deepEquals} for hashing/comparison. */ + protected static class AttributeKey + { + public AttributeKey (Object... attrs) + { + _attrs = attrs; + } + + @Override // documentation inherited + public int hashCode () + { + return Arrays.deepHashCode(_attrs); + } + + @Override // documentation inherited + public boolean equals (Object other) + { + return Arrays.deepEquals(_attrs, ((AttributeKey)other)._attrs); + } + + protected Object[] _attrs; + } /** The name of the texture specified in the model file, which acts as a * property key and a default value. */ diff --git a/src/java/com/threerings/jme/tools/ModelDef.java b/src/java/com/threerings/jme/tools/ModelDef.java index ab1d96e4..1d29fa75 100644 --- a/src/java/com/threerings/jme/tools/ModelDef.java +++ b/src/java/com/threerings/jme/tools/ModelDef.java @@ -39,7 +39,9 @@ import java.util.Set; import com.jme.bounding.BoundingBox; import com.jme.bounding.BoundingSphere; import com.jme.math.FastMath; +import com.jme.math.Vector3f; import com.jme.scene.Spatial; +import com.jme.scene.batch.GeomBatch; import com.jme.util.geom.BufferUtils; import com.samskivert.util.PropertiesUtil; @@ -51,6 +53,7 @@ import com.threerings.jme.model.ModelController; import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelNode; import com.threerings.jme.model.SkinMesh; +import com.threerings.jme.util.SpatialVisitor; /** * An intermediate representation for models used to store data parsed from @@ -63,15 +66,15 @@ public class ModelDef { /** The node's name. */ public String name; - + /** The name of the node's parent. */ public String parent; - + /** The node's transformation. */ public float[] translation; public float[] rotation; public float[] scale; - + /** Returns a JME node for this definition. */ public Spatial getSpatial (Properties props) { @@ -82,7 +85,7 @@ public class ModelDef } return _spatial; } - + /** Sets the transform of the created node. */ protected void setTransform () { @@ -92,10 +95,10 @@ public class ModelDef rotation[2], rotation[3]); _spatial.getLocalScale().set(scale[0], scale[1], scale[2]); } - + /** Creates a JME node for this definition. */ public abstract Spatial createSpatial (Properties props); - + /** Resolves any name references using the supplied map. */ public void resolveReferences ( HashMap nodes, HashSet referenced) @@ -103,17 +106,17 @@ public class ModelDef Spatial pnode = nodes.get(parent); if (pnode instanceof ModelNode) { ((ModelNode)pnode).attachChild(_spatial); - + } else if (parent != null) { Log.warning("Missing or invalid parent node [spatial=" + name + ", parent=" + parent + "]."); } } - + /** The JME node created for this definition. */ protected Spatial _spatial; } - + /** A rigid triangle mesh. */ public static class TriMeshDef extends SpatialDef { @@ -121,25 +124,25 @@ public class ModelDef public float[] offsetTranslation; public float[] offsetRotation; public float[] offsetScale; - + /** Whether or not the mesh allows back face culling. */ public boolean solid; - + /** The texture of the mesh, if any. */ public String texture; - + /** Whether or not the mesh is (partially) transparent. */ public boolean transparent; - + /** The vertices of the mesh. */ public ArrayList vertices = new ArrayList(); - + /** The triangle indices. */ public ArrayList indices = new ArrayList(); - + /** Whether or not any of the vertices have texture coordinates. */ public boolean tcoords; - + public void addVertex (Vertex vertex) { int idx = vertices.indexOf(vertex); @@ -151,7 +154,7 @@ public class ModelDef } tcoords = tcoords || vertex.tcoords != null; } - + // documentation inherited public Spatial createSpatial (Properties props) { @@ -163,13 +166,13 @@ public class ModelDef } return node; } - + /** Creates the mesh to attach to the node. */ protected ModelMesh createMesh () { return new ModelMesh("mesh"); } - + /** Configures the mesh. */ protected void configureMesh (Properties props) { @@ -186,17 +189,17 @@ public class ModelDef _mesh.getLocalScale().set(offsetScale[0], offsetScale[1], offsetScale[2]); } - + // make sure texture is just a filename int sidx = (texture == null) ? -1 : Math.max(texture.lastIndexOf('/'), texture.lastIndexOf('\\')); if (sidx != -1) { texture = texture.substring(sidx + 1); } - + // configure using properties _mesh.configure(solid, texture, transparent, props); - + // set the various buffers int vsize = vertices.size(); FloatBuffer vbuf = BufferUtils.createVector3Buffer(vsize), @@ -210,19 +213,19 @@ public class ModelDef ibuf.put(indices.get(ii)); } _mesh.reconstruct(vbuf, nbuf, null, tbuf, ibuf); - + _mesh.setModelBound("sphere".equals(props.getProperty("bound")) ? new BoundingSphere() : new BoundingBox()); _mesh.updateModelBound(); - + // set the mesh's origin to the center of its bounding box _mesh.centerVertices(); } - + /** The mesh that contains the actual geometry. */ protected ModelMesh _mesh; } - + /** A triangle mesh that deforms according to bone positions. */ public static class SkinMeshDef extends TriMeshDef { @@ -231,7 +234,7 @@ public class ModelDef { return new SkinMesh("mesh"); } - + @Override // documentation inherited public void resolveReferences ( HashMap nodes, HashSet referenced) @@ -240,7 +243,7 @@ public class ModelDef if (_mesh == null) { return; } - + // create and set the final weight groups SkinMesh.WeightGroup[] wgroups = new SkinMesh.WeightGroup[_groups.size()]; @@ -268,7 +271,7 @@ public class ModelDef } ((SkinMesh)_mesh).setWeightGroups(wgroups); } - + @Override // documentation inherited protected void configureMesh (Properties props) { @@ -286,7 +289,7 @@ public class ModelDef group.weights.add(svertex.boneWeights.get(bone).weight); } } - + // reorder the vertices by group ArrayList overts = vertices; vertices = new ArrayList(); @@ -301,14 +304,14 @@ public class ModelDef for (int ii = 0, nn = indices.size(); ii < nn; ii++) { indices.set(ii, imap[indices.get(ii)]); } - + super.configureMesh(props); } - + /** The intermediate weight groups, mapped by bone names. */ protected HashMap, WeightGroupDef> _groups; } - + /** A generic node. */ public static class NodeDef extends SpatialDef { @@ -318,20 +321,20 @@ public class ModelDef return new ModelNode(name); } } - + /** A basic vertex. */ public static class Vertex { public float[] location; public float[] normal; public float[] tcoords; - + public void setInBuffers ( FloatBuffer vbuf, FloatBuffer nbuf, FloatBuffer tbuf) { vbuf.put(location); nbuf.put(normal); - + if (tbuf != null) { if (tcoords != null) { tbuf.put(tcoords); @@ -341,7 +344,7 @@ public class ModelDef } } } - + public boolean equals (Object obj) { Vertex overt = (Vertex)obj; @@ -350,14 +353,14 @@ public class ModelDef Arrays.equals(tcoords, overt.tcoords); } } - + /** A vertex influenced by a number of bones. */ public static class SkinVertex extends Vertex { /** The bones influencing the vertex, mapped by name. */ public HashMap boneWeights = new HashMap(); - + public void addBoneWeight (BoneWeight weight) { if (weight.weight == 0f) { @@ -370,7 +373,7 @@ public class ModelDef boneWeights.put(weight.bone, weight); } } - + /** Finds the bone nodes influencing this vertex. */ public HashSet getBones (HashMap nodes) { @@ -386,7 +389,7 @@ public class ModelDef } return bones; } - + /** Returns the weight of the given bone. */ public float getWeight (ModelNode bone) { @@ -394,13 +397,13 @@ public class ModelDef return (bweight == null) ? 0f : bweight.weight; } } - + /** The influence of a bone on a vertex. */ public static class BoneWeight { /** The name of the influencing bone. */ public String bone; - + /** The amount of influence. */ public float weight; } @@ -410,21 +413,21 @@ public class ModelDef { /** The indices of the affected vertex. */ public ArrayList indices = new ArrayList(); - + /** The interleaved vertex weights. */ public ArrayList weights = new ArrayList(); } - + /** The meshes and bones comprising the model. */ public ArrayList spatials = new ArrayList(); - + public void addSpatial (SpatialDef spatial) { // put nodes before meshes so that bones are updated before skin spatials.add(spatial instanceof NodeDef ? 0 : spatials.size(), spatial); } - + /** * Creates the model node defined herein. * @@ -434,16 +437,13 @@ public class ModelDef public Model createModel (Properties props, HashMap nodes) { Model model = new Model(props.getProperty("name", "model"), props); - - // set the overall scale - model.setLocalScale(Float.parseFloat(props.getProperty("scale", "1"))); - + // start by creating the spatials and mapping them to their names for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { Spatial spatial = spatials.get(ii).getSpatial(props); nodes.put(spatial.getName(), spatial); } - + // then go through again, resolving any name references and attaching // root children HashSet referenced = new HashSet(); @@ -454,12 +454,12 @@ public class ModelDef model.attachChild(sdef.getSpatial(props)); } } - + // create any controllers listed String[] controllers = StringUtil.parseStringArray( props.getProperty("controllers", "")); for (int ii = 0; ii < controllers.length; ii++) { - Properties subProps = + Properties subProps = PropertiesUtil.getSubProperties(props, controllers[ii]); String node = subProps.getProperty("node", controllers[ii]); Spatial target = node.equals(model.getName()) ? @@ -471,15 +471,24 @@ public class ModelDef ModelController ctrl = createController(subProps, target); if (ctrl != null) { model.addController(ctrl); + referenced.add(target); } } - + + // in non-animated models, merge meshes with the same attributes + if (StringUtil.isBlank(props.getProperty("animations"))) { + mergeEquivalentMeshes(model, nodes, referenced); + } + // get rid of any nodes that serve no purpose pruneUnusedNodes(model, nodes, referenced); - + + // set the overall scale + model.setLocalScale(Float.parseFloat(props.getProperty("scale", "1"))); + return model; } - + /** Creates, configures, and returns a model controller. */ protected ModelController createController ( Properties props, Spatial target) @@ -497,7 +506,7 @@ public class ModelDef ctrl.configure(props, target); return ctrl; } - + /** Recursively removes any unused nodes. */ protected boolean pruneUnusedNodes ( ModelNode node, HashMap nodes, @@ -516,7 +525,66 @@ public class ModelDef } return referenced.contains(node) || hasValidChildren; } - + + /** Merges meshes with the same attributes. */ + protected void mergeEquivalentMeshes ( + Model model, HashMap nodes, final HashSet referenced) + { + final HashMap> meshes = + new HashMap>(); + new SpatialVisitor(ModelMesh.class) { + public void traverse (Spatial spatial) { + // cut out when we encounter referenced nodes + if (!referenced.contains(spatial)) { + spatial.updateWorldVectors(); + super.traverse(spatial); + } + } + protected void visit (ModelMesh mesh) { + // make sure we don't try this with skinned meshes + if (mesh instanceof SkinMesh) { + return; + } + Object key = mesh.getAttributeKey(); + ArrayList list = meshes.get(key); + if (list == null) { + meshes.put(key, list = new ArrayList()); + } + list.add(mesh); + } + }.traverse(model); + + for (ArrayList list : meshes.values()) { + // the first in the list becomes the base + ModelMesh base = list.get(0); + flattenTransforms(base); + model.attachChild(base); + + // the rest are merged with it + for (int ii = 1, nn = list.size(); ii < nn; ii++) { + ModelMesh mesh = list.get(ii); + flattenTransforms(mesh); + mesh.getParent().detachChild(mesh); + base.merge(mesh); + } + + // update the model bound and recenter + base.updateModelBound(); + base.centerVertices(); + } + } + + /** Transforms the mesh's vertices and normals into model coordinates. */ + protected static void flattenTransforms (ModelMesh mesh) + { + GeomBatch batch = mesh.getBatch(0); + batch.getWorldCoords(batch.getVertexBuffer()); + batch.getWorldNormals(batch.getNormalBuffer()); + mesh.getLocalTranslation().zero(); + mesh.getLocalRotation().loadIdentity(); + mesh.getLocalScale().set(Vector3f.UNIT_XYZ); + } + /** Converts a boxed Integer list to an unboxed int array. */ protected static int[] toArray (ArrayList list) { @@ -526,7 +594,7 @@ public class ModelDef } return array; } - + /** Converts a boxed Float list to an unboxed float array. */ protected static float[] toArray (ArrayList list) {