db71878346
git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@868 ed5b42cb-e716-0410-a449-f6a68f950b19
1042 lines
36 KiB
Java
1042 lines
36 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Nenya library - tools for developing networked games
|
|
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
|
// http://www.threerings.net/code/nenya/
|
|
//
|
|
// 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.nio.FloatBuffer;
|
|
import java.nio.IntBuffer;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.Map;
|
|
import java.util.Properties;
|
|
import java.util.Set;
|
|
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.Sets;
|
|
|
|
import com.jme.bounding.BoundingBox;
|
|
import com.jme.bounding.BoundingSphere;
|
|
import com.jme.math.FastMath;
|
|
import com.jme.math.Matrix4f;
|
|
import com.jme.math.Quaternion;
|
|
import com.jme.math.Vector3f;
|
|
import com.jme.scene.Node;
|
|
import com.jme.scene.Spatial;
|
|
import com.jme.util.geom.BufferUtils;
|
|
|
|
import com.samskivert.util.ObjectUtil;
|
|
import com.samskivert.util.PropertiesUtil;
|
|
import com.samskivert.util.StringUtil;
|
|
import com.samskivert.util.Tuple;
|
|
|
|
import com.threerings.jme.model.Model;
|
|
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.JmeUtil;
|
|
|
|
import static com.threerings.jme.Log.log;
|
|
|
|
/**
|
|
* An intermediate representation for models used to store data parsed from
|
|
* XML and convert it into JME nodes.
|
|
*/
|
|
public class ModelDef
|
|
{
|
|
/** The base class of nodes in the model. */
|
|
public abstract static class SpatialDef
|
|
{
|
|
/** 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;
|
|
|
|
/**
|
|
* Stores the names of all bones referenced by this spatial in the supplied set.
|
|
*/
|
|
public void getBoneNames (HashSet<String> bones)
|
|
{
|
|
// nothing by default
|
|
}
|
|
|
|
/** Checks whether it's possible (disregarding issues of transformation) to merge
|
|
* the specified spatial into this one. */
|
|
public abstract boolean canMerge (Properties props, SpatialDef other);
|
|
|
|
/** Merges another spatial into this one. */
|
|
public abstract void merge (SpatialDef other, Matrix4f xform);
|
|
|
|
/** Returns a JME node for this definition. */
|
|
public Spatial getSpatial (Properties props)
|
|
{
|
|
if (_spatial == null) {
|
|
_spatial = createSpatial(
|
|
PropertiesUtil.getFilteredProperties(props, name));
|
|
setTransform();
|
|
}
|
|
return _spatial;
|
|
}
|
|
|
|
/** Sets the transform of the created node. */
|
|
protected void setTransform ()
|
|
{
|
|
_spatial.getLocalTranslation().set(translation[0], translation[1],
|
|
translation[2]);
|
|
_spatial.getLocalRotation().set(rotation[0], rotation[1],
|
|
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<String, Spatial> nodes, HashSet<Spatial> referenced)
|
|
{
|
|
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
|
|
{
|
|
/** The geometry offset transform. */
|
|
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 HashArrayList<Vertex> vertices = new HashArrayList<Vertex>();
|
|
|
|
/** The triangle indices. */
|
|
public ArrayList<Integer> indices = Lists.newArrayList();
|
|
|
|
/** Whether or not any of the vertices have texture coordinates. */
|
|
public boolean tcoords;
|
|
|
|
public void addVertex (Vertex vertex)
|
|
{
|
|
int idx = vertices.indexOf(vertex);
|
|
if (idx != -1) {
|
|
indices.add(idx);
|
|
} else {
|
|
indices.add(vertices.size());
|
|
vertices.add(vertex);
|
|
}
|
|
tcoords = tcoords || vertex.tcoords != null;
|
|
}
|
|
|
|
// documentation inherited
|
|
public boolean canMerge (Properties props, SpatialDef other)
|
|
{
|
|
if (getClass() != other.getClass()) {
|
|
return false; // require exact class match
|
|
}
|
|
TriMeshDef omesh = (TriMeshDef)other;
|
|
return solid == omesh.solid && transparent == omesh.transparent &&
|
|
ObjectUtil.equals(texture, omesh.texture) &&
|
|
PropertiesUtil.getSubProperties(props, name).equals(
|
|
PropertiesUtil.getSubProperties(props, omesh.name));
|
|
}
|
|
|
|
// documentation inherited
|
|
public void merge (SpatialDef other, Matrix4f xform)
|
|
{
|
|
TriMeshDef omesh = (TriMeshDef)other;
|
|
|
|
// prepend the inverse of the offset transformation
|
|
xform = getOffsetTransform().invertLocal().multLocal(xform);
|
|
|
|
// and append the other's offset
|
|
xform.multLocal(omesh.getOffsetTransform());
|
|
|
|
// extract the rotation to transform normals
|
|
Quaternion xrot = xform.toRotationQuat();
|
|
|
|
// transform the vertices and add them in
|
|
for (Vertex vertex : omesh.vertices) {
|
|
vertex.transform(xform, xrot);
|
|
}
|
|
for (int idx : omesh.indices) {
|
|
addVertex(omesh.vertices.get(idx));
|
|
}
|
|
}
|
|
|
|
// documentation inherited
|
|
public Spatial createSpatial (Properties props)
|
|
{
|
|
ModelNode node = new ModelNode(name);
|
|
if (indices.size() > 0) {
|
|
_mesh = createMesh();
|
|
optimizeVertexOrder();
|
|
configureMesh(props);
|
|
node.attachChild(_mesh);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/** Gets the matrix representing the offset transform. */
|
|
protected Matrix4f getOffsetTransform ()
|
|
{
|
|
Vector3f otrans = new Vector3f(), oscale = new Vector3f(1f, 1f, 1f);
|
|
Quaternion orot = new Quaternion();
|
|
if (offsetTranslation != null) {
|
|
otrans.set(offsetTranslation[0], offsetTranslation[1], offsetTranslation[2]);
|
|
}
|
|
if (offsetRotation != null) {
|
|
orot.set(offsetRotation[0], offsetRotation[1], offsetRotation[2],
|
|
offsetRotation[3]);
|
|
}
|
|
if (offsetScale != null) {
|
|
oscale.set(offsetScale[0], offsetScale[1], offsetScale[2]);
|
|
}
|
|
return JmeUtil.setTransform(otrans, orot, oscale, new Matrix4f());
|
|
}
|
|
|
|
/** Creates the mesh to attach to the node. */
|
|
protected ModelMesh createMesh ()
|
|
{
|
|
return new ModelMesh("mesh");
|
|
}
|
|
|
|
/** Reorders the vertices to optimize for vertex cache utilization. Uses the algorithm
|
|
* described in Tom Forsyth's article
|
|
* <a href="http://home.comcast.net/~tom_forsyth/papers/fast_vert_cache_opt.html">
|
|
* Linear-Speed Vertex Cache Optimization</a>.
|
|
*/
|
|
protected void optimizeVertexOrder ()
|
|
{
|
|
// start by compiling a list of triangles cross-linked with the vertices they use
|
|
// (we use a linked hash set to ensure consistent iteration order for serialization)
|
|
LinkedHashSet<Triangle> triangles = new LinkedHashSet<Triangle>();
|
|
for (int ii = 0, nn = indices.size(); ii < nn; ii += 3) {
|
|
Vertex[] tverts = new Vertex[] {
|
|
vertices.get(indices.get(ii)),
|
|
vertices.get(indices.get(ii + 1)),
|
|
vertices.get(indices.get(ii + 2))
|
|
};
|
|
Triangle triangle = new Triangle(tverts);
|
|
for (Vertex tvert : tverts) {
|
|
if (tvert.triangles == null) {
|
|
tvert.triangles = Lists.newArrayList();
|
|
}
|
|
tvert.triangles.add(triangle);
|
|
}
|
|
triangles.add(triangle);
|
|
}
|
|
|
|
// init the scores
|
|
for (Vertex vertex : vertices) {
|
|
vertex.updateScore(Integer.MAX_VALUE);
|
|
}
|
|
|
|
// clear the vertices and indices to prepare for readdition
|
|
vertices.clear();
|
|
indices.clear();
|
|
|
|
// while there are triangles remaining, keep adding the one with the best score
|
|
// (as determined by its LRU cache position and number of remaining triangles)
|
|
HashArrayList<Vertex> vcache = new HashArrayList<Vertex>();
|
|
while (!triangles.isEmpty()) {
|
|
// first look for triangles in the cache
|
|
Triangle bestTriangle = null;
|
|
float bestScore = -1f;
|
|
for (Vertex vertex : vcache) {
|
|
for (Triangle triangle : vertex.triangles) {
|
|
float score = triangle.getScore();
|
|
if (score > bestScore) {
|
|
bestTriangle = triangle;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if that didn't work, scan the full list
|
|
if (bestTriangle == null) {
|
|
for (Triangle triangle : triangles) {
|
|
float score = triangle.getScore();
|
|
if (score > bestScore) {
|
|
bestTriangle = triangle;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
}
|
|
|
|
// add and update the vertices from the best triangle
|
|
triangles.remove(bestTriangle);
|
|
for (Vertex vertex : bestTriangle.vertices) {
|
|
addVertex(vertex);
|
|
vertex.triangles.remove(bestTriangle);
|
|
vcache.remove(vertex);
|
|
vcache.add(0, vertex);
|
|
}
|
|
|
|
// update the scores of the vertices in the cache
|
|
for (int ii = 0, nn = vcache.size(); ii < nn; ii++) {
|
|
vcache.get(ii).updateScore(ii);
|
|
}
|
|
|
|
// trim the excess (if any) from the end of the cache
|
|
while (vcache.size() > 64) {
|
|
vcache.remove(vcache.size() - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Configures the mesh. */
|
|
protected void configureMesh (Properties props)
|
|
{
|
|
// set the geometry offset
|
|
if (offsetTranslation != null) {
|
|
_mesh.getLocalTranslation().set(offsetTranslation[0],
|
|
offsetTranslation[1], offsetTranslation[2]);
|
|
}
|
|
if (offsetRotation != null) {
|
|
_mesh.getLocalRotation().set(offsetRotation[0],
|
|
offsetRotation[1], offsetRotation[2], offsetRotation[3]);
|
|
}
|
|
if (offsetScale != null) {
|
|
_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),
|
|
nbuf = BufferUtils.createVector3Buffer(vsize),
|
|
tbuf = tcoords ? BufferUtils.createVector2Buffer(vsize) : null;
|
|
for (int ii = 0; ii < vsize; ii++) {
|
|
vertices.get(ii).setInBuffers(vbuf, nbuf, tbuf);
|
|
}
|
|
IntBuffer ibuf = BufferUtils.createIntBuffer(indices.size());
|
|
for (int ii = 0, nn = indices.size(); ii < nn; ii++) {
|
|
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
|
|
{
|
|
@Override // documentation inherited
|
|
public void getBoneNames (HashSet<String> bones)
|
|
{
|
|
for (Vertex vertex : vertices) {
|
|
bones.addAll(((SkinVertex)vertex).boneWeights.keySet());
|
|
}
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
protected ModelMesh createMesh ()
|
|
{
|
|
return new SkinMesh("mesh");
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public void resolveReferences (
|
|
HashMap<String, Spatial> nodes, HashSet<Spatial> referenced)
|
|
{
|
|
super.resolveReferences(nodes, referenced);
|
|
if (_mesh == null) {
|
|
return;
|
|
}
|
|
|
|
// create and set the final weight groups
|
|
SkinMesh.WeightGroup[] wgroups = new SkinMesh.WeightGroup[_groups.size()];
|
|
HashMap<String, SkinMesh.Bone> bones = Maps.newHashMap();
|
|
int ii = 0;
|
|
int mweights = 0, tweights = 0;
|
|
for (Map.Entry<Set<String>, WeightGroupDef> entry :
|
|
_groups.entrySet()) {
|
|
SkinMesh.WeightGroup wgroup = new SkinMesh.WeightGroup();
|
|
wgroup.vertexCount = entry.getValue().indices.size();
|
|
wgroup.bones = new SkinMesh.Bone[entry.getKey().size()];
|
|
int jj = 0;
|
|
for (String bname : entry.getKey()) {
|
|
SkinMesh.Bone bone = bones.get(bname);
|
|
if (bone == null) {
|
|
Spatial node = nodes.get(bname);
|
|
bones.put(bname,
|
|
bone = new SkinMesh.Bone((ModelNode)node));
|
|
referenced.add(node);
|
|
}
|
|
wgroup.bones[jj++] = bone;
|
|
}
|
|
wgroup.weights = toArray(entry.getValue().weights);
|
|
tweights += wgroup.bones.length;
|
|
mweights = Math.max(wgroup.bones.length, mweights);
|
|
wgroups[ii++] = wgroup;
|
|
}
|
|
((SkinMesh)_mesh).setWeightGroups(wgroups);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
protected void configureMesh (Properties props)
|
|
{
|
|
// divide the vertices up by weight groups
|
|
_groups = Maps.newHashMap();
|
|
for (int ii = 0, nn = vertices.size(); ii < nn; ii++) {
|
|
SkinVertex svertex = (SkinVertex)vertices.get(ii);
|
|
Set<String> bones = svertex.boneWeights.keySet();
|
|
WeightGroupDef group = _groups.get(bones);
|
|
if (group == null) {
|
|
_groups.put(bones, group = new WeightGroupDef());
|
|
}
|
|
group.indices.add(ii);
|
|
for (String bone : bones) {
|
|
group.weights.add(svertex.boneWeights.get(bone).weight);
|
|
}
|
|
}
|
|
|
|
// reorder the vertices by group
|
|
ArrayList<Vertex> overts = vertices;
|
|
vertices = new HashArrayList<Vertex>();
|
|
int[] imap = new int[overts.size()];
|
|
for (Map.Entry<Set<String>, WeightGroupDef> entry :
|
|
_groups.entrySet()) {
|
|
for (int idx : entry.getValue().indices) {
|
|
imap[idx] = vertices.size();
|
|
vertices.add(overts.get(idx));
|
|
}
|
|
}
|
|
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<Set<String>, WeightGroupDef> _groups;
|
|
}
|
|
|
|
/** A generic node. */
|
|
public static class NodeDef extends SpatialDef
|
|
{
|
|
// documentation inherited
|
|
public boolean canMerge (Properties props, SpatialDef other)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// documentation inherited
|
|
public void merge (SpatialDef other, Matrix4f xform)
|
|
{
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
// documentation inherited
|
|
public Spatial createSpatial (Properties props)
|
|
{
|
|
return new ModelNode(name);
|
|
}
|
|
}
|
|
|
|
/** Represents a triangle for processing purposes. */
|
|
public static class Triangle
|
|
{
|
|
public Vertex[] vertices;
|
|
|
|
public Triangle (Vertex[] vertices)
|
|
{
|
|
this.vertices = vertices;
|
|
}
|
|
|
|
public float getScore ()
|
|
{
|
|
return vertices[0].score + vertices[1].score + vertices[2].score;
|
|
}
|
|
}
|
|
|
|
/** A basic vertex. */
|
|
public static class Vertex
|
|
{
|
|
public float[] location;
|
|
public float[] normal;
|
|
public float[] tcoords;
|
|
|
|
public ArrayList<Triangle> triangles;
|
|
public float score;
|
|
|
|
public void updateScore (int cacheIdx)
|
|
{
|
|
float pscore;
|
|
if (cacheIdx > 63) {
|
|
pscore = 0f; // outside the cache
|
|
} else if (cacheIdx < 3) {
|
|
pscore = 0.75f; // the three most recent vertices
|
|
} else {
|
|
pscore = FastMath.pow((63 - cacheIdx) / 60f, 1.5f);
|
|
}
|
|
score = pscore + 2f * FastMath.pow(triangles.size(), -0.5f);
|
|
}
|
|
|
|
public void transform (Matrix4f xform, Quaternion xrot)
|
|
{
|
|
Vector3f xvec = new Vector3f(location[0], location[1], location[2]);
|
|
xform.mult(xvec, xvec);
|
|
location[0] = xvec.x;
|
|
location[1] = xvec.y;
|
|
location[2] = xvec.z;
|
|
|
|
xvec.set(normal[0], normal[1], normal[2]);
|
|
xrot.mult(xvec, xvec);
|
|
normal[0] = xvec.x;
|
|
normal[1] = xvec.y;
|
|
normal[2] = xvec.z;
|
|
}
|
|
|
|
public void setInBuffers (
|
|
FloatBuffer vbuf, FloatBuffer nbuf, FloatBuffer tbuf)
|
|
{
|
|
vbuf.put(location);
|
|
nbuf.put(normal);
|
|
|
|
if (tbuf != null) {
|
|
if (tcoords != null) {
|
|
tbuf.put(tcoords);
|
|
} else {
|
|
tbuf.put(0f);
|
|
tbuf.put(0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
public String toString ()
|
|
{
|
|
return StringUtil.toString(location);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public int hashCode ()
|
|
{
|
|
return Arrays.hashCode(location) ^ Arrays.hashCode(normal) ^ Arrays.hashCode(tcoords);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public boolean equals (Object obj)
|
|
{
|
|
Vertex overt = (Vertex)obj;
|
|
return Arrays.equals(location, overt.location) &&
|
|
Arrays.equals(normal, overt.normal) &&
|
|
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<String, BoneWeight> boneWeights = Maps.newHashMap();
|
|
|
|
public void addBoneWeight (BoneWeight weight)
|
|
{
|
|
if (weight.weight == 0f) {
|
|
return;
|
|
}
|
|
BoneWeight bweight = boneWeights.get(weight.bone);
|
|
if (bweight != null) {
|
|
bweight.weight += weight.weight;
|
|
} else {
|
|
boneWeights.put(weight.bone, weight);
|
|
}
|
|
}
|
|
|
|
/** Finds the bone nodes influencing this vertex. */
|
|
public HashSet<ModelNode> getBones (HashMap<String, Spatial> nodes)
|
|
{
|
|
HashSet<ModelNode> bones = Sets.newHashSet();
|
|
for (String bone : boneWeights.keySet()) {
|
|
Spatial node = nodes.get(bone);
|
|
if (node instanceof ModelNode) {
|
|
bones.add((ModelNode)node);
|
|
} else {
|
|
log.warning("Missing or invalid bone for bone weight " +
|
|
"[bone=" + bone + "].");
|
|
}
|
|
}
|
|
return bones;
|
|
}
|
|
|
|
/** Returns the weight of the given bone. */
|
|
public float getWeight (ModelNode bone)
|
|
{
|
|
BoneWeight bweight = boneWeights.get(bone.getName());
|
|
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;
|
|
}
|
|
|
|
/** A group of vertices influenced by the same bone. */
|
|
public static class WeightGroupDef
|
|
{
|
|
/** The indices of the affected vertex. */
|
|
public ArrayList<Integer> indices = Lists.newArrayList();
|
|
|
|
/** The interleaved vertex weights. */
|
|
public ArrayList<Float> weights = Lists.newArrayList();
|
|
}
|
|
|
|
/** Contains the transform of a node for preprocessing. */
|
|
public static class TransformNode extends Node
|
|
{
|
|
/** The source definition. */
|
|
public SpatialDef spatial;
|
|
|
|
/** If true, this node is referenced by name (as a bone or parent) and cannot be merged
|
|
* into another. */
|
|
public boolean referenced;
|
|
|
|
/** If true, this node is a controller target; nodes beneath it can only be merged with
|
|
* other descendants. */
|
|
public boolean controlled;
|
|
|
|
/** The node's current local transform. */
|
|
public Matrix4f localTransform = new Matrix4f();
|
|
|
|
/** The node's current world space transform. */
|
|
public Matrix4f worldTransform = new Matrix4f();
|
|
|
|
/** The node's local transform in the original model, or <code>null</code> if the node's
|
|
* transform has diverged from the original. */
|
|
public Matrix4f baseLocalTransform;
|
|
|
|
/** The relative transforms between this and all other loosely compatible nodes not yet
|
|
* eliminated. As soon as the relative transform diverges in the course of preprocessing
|
|
* an animation, the node/transform pair is removed from the list. */
|
|
public ArrayList<Tuple<TransformNode, Matrix4f>> relativeTransforms;
|
|
|
|
/** Marks this node as having been transformed in the course of an animation. */
|
|
public boolean transformed;
|
|
|
|
public TransformNode (SpatialDef spatial)
|
|
{
|
|
super(spatial.name);
|
|
this.spatial = spatial;
|
|
setLocalTransform(spatial.translation, spatial.rotation, spatial.scale);
|
|
}
|
|
|
|
public void setLocalTransform (float[] translation, float[] rotation, float[] scale)
|
|
{
|
|
getLocalTranslation().set(translation[0], translation[1], translation[2]);
|
|
getLocalRotation().set(rotation[0], rotation[1], rotation[2], rotation[3]);
|
|
getLocalScale().set(scale[0], scale[1], scale[2]);
|
|
JmeUtil.setTransform(
|
|
getLocalTranslation(), getLocalRotation(), getLocalScale(), localTransform);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public void updateWorldVectors ()
|
|
{
|
|
super.updateWorldVectors();
|
|
JmeUtil.setTransform(
|
|
getWorldTranslation(), getWorldRotation(), getWorldScale(), worldTransform);
|
|
}
|
|
|
|
public boolean canMerge (Properties props, TransformNode onode)
|
|
{
|
|
// nodes must have same controlled ancestor
|
|
return !onode.referenced && spatial.canMerge(props, onode.spatial) &&
|
|
getControlledAncestor() == onode.getControlledAncestor();
|
|
}
|
|
|
|
protected Node getControlledAncestor ()
|
|
{
|
|
Node ref = this;
|
|
while (ref instanceof TransformNode && !((TransformNode)ref).controlled) {
|
|
ref = ref.getParent();
|
|
}
|
|
return ref;
|
|
}
|
|
|
|
public void cullDivergentTransforms ()
|
|
{
|
|
if (baseLocalTransform != null && !epsilonEquals(localTransform, baseLocalTransform)) {
|
|
baseLocalTransform = null;
|
|
}
|
|
for (Iterator<Tuple<TransformNode, Matrix4f>> it = relativeTransforms.iterator();
|
|
it.hasNext(); ) {
|
|
Tuple<TransformNode, Matrix4f> tuple = it.next();
|
|
if (!epsilonEquals(getRelativeTransform(tuple.left), tuple.right)) {
|
|
it.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
public Matrix4f getRelativeTransform (TransformNode other)
|
|
{
|
|
// return the matrix that takes vertices from the space of the other node
|
|
// into the space of this one
|
|
Matrix4f inv = new Matrix4f();
|
|
worldTransform.invert(inv);
|
|
return inv.mult(other.worldTransform);
|
|
}
|
|
}
|
|
|
|
/** The meshes and bones comprising the model. */
|
|
public ArrayList<SpatialDef> spatials = Lists.newArrayList();
|
|
|
|
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 and returns a transform tree representing the model for preprocessing.
|
|
*/
|
|
public Node createTransformTree (Properties props, HashMap<String, TransformNode> nodes)
|
|
{
|
|
// create the nodes and map them by name
|
|
for (SpatialDef spatial : spatials) {
|
|
nodes.put(spatial.name, new TransformNode(spatial));
|
|
}
|
|
|
|
// resolve the parents and collect the names of the bones
|
|
Node root = new Node("root");
|
|
HashSet<String> bones = Sets.newHashSet();
|
|
for (TransformNode node : nodes.values()) {
|
|
if (node.spatial.parent == null) {
|
|
root.attachChild(node);
|
|
} else {
|
|
TransformNode pnode = nodes.get(node.spatial.parent);
|
|
if (pnode != null) {
|
|
pnode.attachChild(node);
|
|
pnode.referenced = true;
|
|
}
|
|
}
|
|
node.spatial.getBoneNames(bones);
|
|
}
|
|
|
|
// mark the bones as referenced
|
|
for (String name : bones) {
|
|
TransformNode node = nodes.get(name);
|
|
if (node != null) {
|
|
node.referenced = true;
|
|
}
|
|
}
|
|
|
|
// mark the controlled nodes
|
|
String[] controllers = StringUtil.parseStringArray(props.getProperty("controllers", ""));
|
|
for (String controller : controllers) {
|
|
Properties subProps = PropertiesUtil.getSubProperties(props, controller);
|
|
TransformNode node = nodes.get(subProps.getProperty("node", controller));
|
|
if (node != null) {
|
|
node.referenced = node.controlled = true;
|
|
}
|
|
}
|
|
|
|
// store the base transforms and relative transforms for merge candidates
|
|
root.updateGeometricState(0f, true);
|
|
for (TransformNode node : nodes.values()) {
|
|
node.baseLocalTransform = new Matrix4f(node.localTransform);
|
|
node.relativeTransforms = Lists.newArrayList();
|
|
for (TransformNode onode : nodes.values()) {
|
|
if (node == onode || !node.canMerge(props, onode)) {
|
|
continue;
|
|
}
|
|
node.relativeTransforms.add(new Tuple<TransformNode, Matrix4f>(
|
|
onode, node.getRelativeTransform(onode)));
|
|
}
|
|
}
|
|
|
|
return root;
|
|
}
|
|
|
|
/**
|
|
* Merges compatible meshes that retain the same relative transform throughout all animations.
|
|
*/
|
|
public void mergeSpatials (HashMap<String, TransformNode> nodes)
|
|
{
|
|
for (TransformNode node : nodes.values()) {
|
|
if (!spatials.contains(node.spatial)) {
|
|
continue;
|
|
}
|
|
for (Tuple<TransformNode, Matrix4f> tuple : node.relativeTransforms) {
|
|
if (spatials.contains(tuple.left.spatial)) {
|
|
node.spatial.merge(tuple.left.spatial, tuple.right);
|
|
spatials.remove(tuple.left.spatial);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the model node defined herein.
|
|
*
|
|
* @param props the properties of the model
|
|
* @param nodes a node map to populate
|
|
*/
|
|
public Model createModel (Properties props, HashMap<String, Spatial> nodes)
|
|
{
|
|
Model model = new Model(props.getProperty("name", "model"), props);
|
|
|
|
// 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<Spatial> referenced = Sets.newHashSet();
|
|
for (int ii = 0, nn = spatials.size(); ii < nn; ii++) {
|
|
SpatialDef sdef = spatials.get(ii);
|
|
sdef.resolveReferences(nodes, referenced);
|
|
if (sdef.getSpatial(props).getParent() == null) {
|
|
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 =
|
|
PropertiesUtil.getSubProperties(props, controllers[ii]);
|
|
String node = subProps.getProperty("node", controllers[ii]);
|
|
Spatial target = node.equals(model.getName()) ?
|
|
model : nodes.get(node);
|
|
if (target == null) {
|
|
log.warning("Missing controller node [name=" + node + "].");
|
|
continue;
|
|
}
|
|
ModelController ctrl = createController(subProps, target);
|
|
if (ctrl != null) {
|
|
model.addController(ctrl);
|
|
referenced.add(target);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// attempt to create an instance of the controller
|
|
ModelController ctrl;
|
|
String cname = props.getProperty("class", "");
|
|
try {
|
|
ctrl = (ModelController)Class.forName(cname).newInstance();
|
|
} catch (Exception e) {
|
|
log.warning("Error instantiating controller [class=" + cname +
|
|
", error=" + e + "].");
|
|
return null;
|
|
}
|
|
ctrl.configure(props, target);
|
|
return ctrl;
|
|
}
|
|
|
|
/** Recursively removes any unused nodes. */
|
|
protected boolean pruneUnusedNodes (
|
|
ModelNode node, HashMap<String, Spatial> nodes,
|
|
HashSet<Spatial> referenced)
|
|
{
|
|
boolean hasValidChildren = false;
|
|
for (int ii = node.getQuantity() - 1; ii >= 0; ii--) {
|
|
Spatial child = node.getChild(ii);
|
|
if (!(child instanceof ModelNode) ||
|
|
pruneUnusedNodes((ModelNode)child, nodes, referenced)) {
|
|
hasValidChildren = true;
|
|
} else {
|
|
node.detachChildAt(ii);
|
|
nodes.remove(child.getName());
|
|
}
|
|
}
|
|
return referenced.contains(node) || hasValidChildren;
|
|
}
|
|
|
|
/** Determines whether a pair of matrices are "close enough" to equal. */
|
|
public static boolean epsilonEquals (Matrix4f m1, Matrix4f m2)
|
|
{
|
|
for (int ii = 0; ii < 4; ii++) {
|
|
for (int jj = 0; jj < 4; jj++) {
|
|
if (FastMath.abs(m1.get(ii, jj) - m2.get(ii, jj)) > 0.0001f) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Converts a boxed Integer list to an unboxed int array. */
|
|
protected static int[] toArray (ArrayList<Integer> list)
|
|
{
|
|
int[] array = new int[list.size()];
|
|
for (int ii = 0, nn = list.size(); ii < nn; ii++) {
|
|
array[ii] = list.get(ii);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/** Converts a boxed Float list to an unboxed float array. */
|
|
protected static float[] toArray (ArrayList<Float> list)
|
|
{
|
|
float[] array = new float[list.size()];
|
|
for (int ii = 0, nn = list.size(); ii < nn; ii++) {
|
|
array[ii] = list.get(ii);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/** Accelerates {@link ArrayList#indexOf}, {@link ArrayList#contains}, and
|
|
* {@link ArrayList#remove} using an internal hash map (assumes that all elements of the list
|
|
* are unique and non-null). */
|
|
protected static class HashArrayList<E> extends ArrayList<E>
|
|
{
|
|
@Override // documentation inherited
|
|
public boolean add (E element)
|
|
{
|
|
add(size(), element);
|
|
return true;
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public void add (int idx, E element)
|
|
{
|
|
super.add(idx, element);
|
|
remapFrom(idx);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public E remove (int idx)
|
|
{
|
|
E element = super.remove(idx);
|
|
_indices.remove(element);
|
|
remapFrom(idx);
|
|
return element;
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public void clear ()
|
|
{
|
|
super.clear();
|
|
_indices.clear();
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public int indexOf (Object obj)
|
|
{
|
|
Integer idx = _indices.get(obj);
|
|
return (idx == null ? -1 : idx);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public boolean contains (Object obj)
|
|
{
|
|
return _indices.containsKey(obj);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public boolean remove (Object obj)
|
|
{
|
|
Integer idx = _indices.remove(obj);
|
|
if (idx != null) {
|
|
super.remove(idx);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected void remapFrom (int idx)
|
|
{
|
|
for (int ii = idx, nn = size(); ii < nn; ii++) {
|
|
_indices.put(get(ii), ii);
|
|
}
|
|
}
|
|
|
|
/** Maps elements to their indices in the list. */
|
|
protected HashMap<Object, Integer> _indices = Maps.newHashMap();
|
|
}
|
|
}
|