2728af5059
space bone transforms only once per mesh per bone (rather than each time the bone appears in a weight group), low level fiddling. Also changed the exporters to export everything, regardless of what's selected. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4081 542714f4-19e9-0310-aa3c-eee0fc999fb1
574 lines
20 KiB
Java
574 lines
20 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.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.nio.FloatBuffer;
|
|
import java.nio.IntBuffer;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.Map;
|
|
import java.util.Properties;
|
|
import java.util.Set;
|
|
|
|
import com.jme.bounding.BoundingBox;
|
|
import com.jme.bounding.BoundingSphere;
|
|
import com.jme.math.FastMath;
|
|
import com.jme.scene.Spatial;
|
|
import com.jme.util.geom.BufferUtils;
|
|
|
|
import com.samskivert.util.PropertiesUtil;
|
|
import com.samskivert.util.StringUtil;
|
|
|
|
import com.threerings.jme.Log;
|
|
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;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/** Returns a JME node for this definition. */
|
|
public Spatial getSpatial (Properties props)
|
|
{
|
|
if (_spatial == null) {
|
|
_spatial = createSpatial(new NodeProperties(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 ArrayList<Vertex> vertices = new ArrayList<Vertex>();
|
|
|
|
/** The triangle indices. */
|
|
public ArrayList<Integer> indices = new ArrayList<Integer>();
|
|
|
|
/** 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 Spatial createSpatial (Properties props)
|
|
{
|
|
ModelNode node = new ModelNode(name);
|
|
if (indices.size() > 0) {
|
|
_mesh = createMesh();
|
|
configureMesh(props);
|
|
node.attachChild(_mesh);
|
|
}
|
|
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)
|
|
{
|
|
// 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();
|
|
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;
|
|
for (int ii = 0; ii < vsize; ii++) {
|
|
vertices.get(ii).setInBuffers(vbuf, nbuf, tbuf);
|
|
}
|
|
IntBuffer ibuf = ibbuf.asIntBuffer();
|
|
for (int ii = 0, nn = indices.size(); ii < nn; ii++) {
|
|
ibuf.put(indices.get(ii));
|
|
}
|
|
_mesh.reconstruct(vbbuf, nbbuf, null, tbbuf, ibbuf);
|
|
|
|
_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
|
|
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 =
|
|
new HashMap<String, SkinMesh.Bone>();
|
|
int ii = 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);
|
|
wgroups[ii++] = wgroup;
|
|
}
|
|
((SkinMesh)_mesh).setWeightGroups(wgroups);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
protected void configureMesh (Properties props)
|
|
{
|
|
// divide the vertices up by weight groups
|
|
_groups = new HashMap<Set<String>, WeightGroupDef>();
|
|
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 ArrayList<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 Spatial createSpatial (Properties props)
|
|
{
|
|
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);
|
|
} else {
|
|
tbuf.put(0f);
|
|
tbuf.put(0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 =
|
|
new HashMap<String, BoneWeight>();
|
|
|
|
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 = new HashSet<ModelNode>();
|
|
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 = new ArrayList<Integer>();
|
|
|
|
/** The interleaved vertex weights. */
|
|
public ArrayList<Float> weights = new ArrayList<Float>();
|
|
}
|
|
|
|
/** The meshes and bones comprising the model. */
|
|
public ArrayList<SpatialDef> spatials = new ArrayList<SpatialDef>();
|
|
|
|
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.
|
|
*
|
|
* @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);
|
|
|
|
// 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<Spatial> referenced = new HashSet<Spatial>();
|
|
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++) {
|
|
Spatial target = nodes.get(controllers[ii]);
|
|
if (target == null) {
|
|
Log.warning("Missing controller node [name=" +
|
|
controllers[ii] + "].");
|
|
continue;
|
|
}
|
|
ModelController ctrl = createController(
|
|
PropertiesUtil.getSubProperties(props, controllers[ii]),
|
|
target);
|
|
if (ctrl != null) {
|
|
model.addController(ctrl);
|
|
}
|
|
}
|
|
|
|
// get rid of any nodes that serve no purpose
|
|
pruneUnusedNodes(model, nodes, referenced);
|
|
|
|
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 (Iterator it = node.getChildren().iterator(); it.hasNext(); ) {
|
|
Spatial child = (Spatial)it.next();
|
|
if (!(child instanceof ModelNode) ||
|
|
pruneUnusedNodes((ModelNode)child, nodes, referenced)) {
|
|
hasValidChildren = true;
|
|
} else {
|
|
it.remove();
|
|
nodes.remove(child.getName());
|
|
}
|
|
}
|
|
return referenced.contains(node) || hasValidChildren;
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
/** A wrapper for the model properties providing access to the properties
|
|
* of a node within the model. */
|
|
protected static class NodeProperties extends Properties
|
|
{
|
|
public NodeProperties (Properties mprops, String name)
|
|
{
|
|
_mprops = mprops;
|
|
_prefix = name + ".";
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public String getProperty (String key)
|
|
{
|
|
return getProperty(key, null);
|
|
}
|
|
|
|
@Override // documentation inherited
|
|
public String getProperty (String key, String defaultValue)
|
|
{
|
|
return _mprops.getProperty(_prefix + key,
|
|
_mprops.getProperty(key, defaultValue));
|
|
}
|
|
|
|
/** The properties of the model. */
|
|
protected Properties _mprops;
|
|
|
|
/** The node prefix. */
|
|
protected String _prefix;
|
|
}
|
|
}
|