OSDN Git Service

Morph implementation
authorblaine.dev <blaine.dev@75d07b2b-3a1a-0410-a2c5-0572b91ccdca>
Sun, 26 Jul 2009 18:44:33 +0000 (18:44 +0000)
committerblaine.dev <blaine.dev@75d07b2b-3a1a-0410-a2c5-0572b91ccdca>
Sun, 26 Jul 2009 18:44:33 +0000 (18:44 +0000)
git-svn-id: http://jmonkeyengine.googlecode.com/svn/trunk@4553 75d07b2b-3a1a-0410-a2c5-0572b91ccdca

src/com/jme/scene/MorphInfluencesMapProvider.java [new file with mode: 0644]
src/com/jme/scene/MorphingGeometry.java [new file with mode: 0644]
src/com/jme/scene/MorphingTriMesh.java [new file with mode: 0644]

diff --git a/src/com/jme/scene/MorphInfluencesMapProvider.java b/src/com/jme/scene/MorphInfluencesMapProvider.java
new file mode 100644 (file)
index 0000000..5746d0e
--- /dev/null
@@ -0,0 +1,15 @@
+package com.jme.scene;
+
+import java.util.Map;
+
+import com.jme.util.export.ListenableStringFloatMap;
+
+/**
+ * An item that shares a ListenableStringFloatMap.
+ */
+public interface MorphInfluencesMapProvider {
+    /**
+     * @return The morph influences map controlled by this Provider.
+     */
+    public ListenableStringFloatMap getMorphInfluencesMap();
+}
diff --git a/src/com/jme/scene/MorphingGeometry.java b/src/com/jme/scene/MorphingGeometry.java
new file mode 100644 (file)
index 0000000..ea17255
--- /dev/null
@@ -0,0 +1,100 @@
+package com.jme.scene;
+
+import java.util.Map;
+
+import com.jme.util.export.ListenableStringFloatMap;
+import com.jme.scene.Geometry;
+import com.jme.animation.SkinNode;
+
+/**
+ * A derived Geometry that generates its data from a set of component
+ * Geometries (morphs) which are not attached to any scene.
+ * <P>
+ * <b>Each morph Geometry must have the same quantity of vertexes (and normals
+ *  and vertex colors).</b>  Most other data, including the vertex indexes,
+ *  are taken from the Primary Morph, ignoring these data from the other morphs.
+ * <P> </P>
+ * Data which is not morphed comes directly from the first (primary) morph
+ * Geometry.
+ * <P> </P>
+ * The primary skin is the base, and is modified by each additional skin morph
+ * in order.
+ * </P>
+ *
+ * @author Blaine Simpson (blaine dot simpson at admc dot com)
+ */
+public interface MorphingGeometry
+        extends ListenableStringFloatMap.FloatListener {
+    /**
+     * Add a component Geometry morph with same quantity of vertexes as the
+     * primary morph.
+     * Do not use this method to set the primary morph.
+     *
+     * @param morphKey  Key in the influences map for the influence for this
+     *                  morph.
+     */
+    public void addMorph(String morphKey, Geometry mesh);
+
+    /**
+     * Use setMorphInfluences if you want to change multiple floats, to avoid
+     * unnecessary listener callbacks.
+     *
+     * @see #setMorphInfluences(Map<? extends String, ? extends Float)
+     */
+    public void setSingleMorphInfluence(String morphKey, float influence);
+
+    /**
+     * Assign morph influence values.
+     */
+    public void setMorphInfluences(Map<? extends String, ? extends Float> m);
+
+    /**
+     * If we have an influences map before you invoke this method (either local
+     * or remote), it will be retained unless we successfully find delegate to
+     * another (in which case 'yes' will be returned).
+     *
+     * @return true if we successfully fetched a new influences map from a
+     * provider.
+     */
+    public boolean delegateInfluences();
+
+    /**
+     * Set a local morph influences map.
+     * If there were an influences map in use before (local or remote), it
+     * will be removed or disassociated-from first.
+     */
+    public void setMorphInfluencesMap(ListenableStringFloatMap m);
+
+    /**
+     * @return The morph influences map controlling the MorphingGeometry.
+     *         It may be a non-local (delegated) influences map.
+     */
+    public ListenableStringFloatMap getMorphInfluencesMap();
+
+    /**
+     * Morphs (verb) if any of the morphs (noun) or any of the morph
+     * influence values have changed.
+     *
+     * This method should only be called from an update thread.
+     */
+    public void morph();
+
+    /**
+     * Unconditionally merges the relevant floats from FloabBuffers of the
+     * component morphs.
+     *
+     * This method should only be called from an update thread.
+     */
+    public void forceMorph();
+
+    /**
+     * Causes morph merges to automatically occur as needed during update loops.
+     */
+    public void setAutoMorph(boolean autoMorph);
+
+    /**
+     * @return Grandparent SkinNode IFF this MorphingGeometry is a currently-
+     *         attached Skin geometry.
+     */
+    public SkinNode getSkinNode(); 
+}
diff --git a/src/com/jme/scene/MorphingTriMesh.java b/src/com/jme/scene/MorphingTriMesh.java
new file mode 100644 (file)
index 0000000..7d91960
--- /dev/null
@@ -0,0 +1,455 @@
+package com.jme.scene;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+import com.jme.scene.TriMesh;
+import com.jme.animation.SkinNode;
+import com.jme.scene.Controller;
+import com.jme.scene.Geometry;
+import com.jme.scene.Node;
+import com.jme.math.Quaternion;
+import com.jme.math.Vector3f;
+import com.jme.scene.state.RenderState;
+import com.jme.renderer.Renderer;
+import com.jme.util.export.StringFloatMap;
+import com.jme.util.export.InputCapsule;
+import com.jme.util.export.JMEExporter;
+import com.jme.util.export.JMEImporter;
+import com.jme.util.export.OutputCapsule;
+import com.jme.util.geom.BufferUtils;
+import com.jme.util.export.ListenableStringFloatMap;
+
+/**
+ * A MorphingGeometry implementation for TriMesh component morph Geometries.
+ *
+ * TODO:  Optimize the instantiation procedure.  It's pretty complicated to get
+ * to reconstitute or instantiate local and delegated influences properly
+ * without invoking expensive but unnecessary forceMorphs.
+ *
+ * @see #MorphingGeometry
+ * @author Blaine Simpson (blaine dot simpson at admc dot com)
+ */
+public class MorphingTriMesh extends TriMesh implements MorphingGeometry {
+    private static final Logger logger =
+            Logger.getLogger(MorphingTriMesh.class.getName());
+
+    protected List<TriMesh> morphs = new ArrayList<TriMesh>();
+    protected List<String> morphKeys = new ArrayList<String>();
+    // Not using an ordered Map of some type for these paired lists, only
+    // because of the atrocious J2SE support for Maps which "preserve" order
+    // but do not "apply" an order (I.e. to just preserve the order in which
+    // the items are added).
+    protected TriMesh primaryMorph;
+    volatile private boolean dirty = true;
+
+    /** This one may be a reference to a remotely managed map */
+    protected ListenableStringFloatMap morphInfluencesMap;
+
+    /** This one is persisted with this MorphingTriMesh instance */
+    protected ListenableStringFloatMap localMorphInfluencesMap;
+
+    /**
+     * @see #MorphingGeometry#getMorphInfluencesMap()
+     */
+    public ListenableStringFloatMap getMorphInfluencesMap() {
+        return morphInfluencesMap;
+    }
+
+    /**
+     * @see #MorphingGeometry#setMorphInfluencesMap(ListenableStringFloatMap)
+     */
+    public void setMorphInfluencesMap(ListenableStringFloatMap m) {
+        if (localMorphInfluencesMap == m) return;
+        if (localMorphInfluencesMap != null)
+            localMorphInfluencesMap.removeListener(this);
+        localMorphInfluencesMap = m;
+        localMorphInfluencesMap.addListener(this, morphKeys);
+        morphInfluencesMap = localMorphInfluencesMap;
+    }
+
+    /**
+     * @see #MorphingGeometry#setSingleMorphInfluence(String, float)
+     */
+    public void setSingleMorphInfluence(String morphKey, float influence) {
+        if (morphInfluencesMap == null)
+            throw new IllegalStateException("No morphInfluences set");
+        morphInfluencesMap.put(morphKey, influence);
+    }
+
+    /**
+     * @see #MorphingGeometry#setMorphInfluences(
+     *           Map<? extends String, ? extends Float>)
+     */
+    public void setMorphInfluences(Map<? extends String, ? extends Float> m) {
+        if (morphInfluencesMap == null)
+            throw new IllegalStateException("No morphInfluences set");
+        morphInfluencesMap.putAll(m);
+    }
+
+    /**
+     * @see #MorphingGeometry#delegateInfluences()
+     */
+    public boolean delegateInfluences() {
+        ListenableStringFloatMap newInfluencesMap = null;
+        Node ancestor = null;
+        for (ancestor = getParent(); ancestor != null;
+                ancestor = ancestor.getParent()) {
+            logger.log(Level.FINEST, "Trying anc '{0}'...", ancestor.getName());
+            if (!(ancestor instanceof MorphInfluencesMapProvider)) continue;
+            newInfluencesMap =
+                ((MorphInfluencesMapProvider) ancestor).getMorphInfluencesMap();
+            if (newInfluencesMap != null) break;
+        }
+        if (newInfluencesMap == null) {
+            logger.info("Failed to find a parent node to delegate to");
+            return false;
+        }
+        if (morphInfluencesMap == newInfluencesMap) {
+            logger.fine("The first MorphInfluencesMapProvider found has the "
+                    + "same MorphInfluencesMap that we are already using");
+            return false;
+        }
+        morphInfluencesMap = newInfluencesMap;
+        morphInfluencesMap.addListener(this, morphKeys);
+        logger.log(Level.INFO, "Delegating influences to '{1}'", ancestor);
+        return true;
+    }
+
+    /**
+     * Constructor for internal use only.
+     */
+    public MorphingTriMesh() { }
+
+    /**
+     * Normal constructor.
+     *
+     * For local influence control, you should run <PRE><CODE>
+     *     morphingTriMesh.addMorph(...);... // anytime before morph()ing
+     *     morphingTriMesh.setMorphInfluencesMap(
+     *             new ListenableStringFloatMap());
+     *     morphingTriMesh.setMorphInfluences(...);
+     *     morphingTriMesh.morph();
+     * </CODE></PRE>
+     * before attaching to a live scene.
+     * For remote influence control, if you know there is an available
+     * ancestor Node MorphInfluencesMapProvider, run
+     * <PRE><CODE>
+     *     morphingTriMesh.addMorph(...);... // anytime before morph()ing
+     *     morphingTriMesh.delegateInfluences();
+     *     morphingTriMesh.morph();
+     * </CODE></PRE>
+     * between attaching to the scene and rendering (like in a single update()
+     * run).
+     * <P>
+     * If you want to delegate but aren't certain a capable ancestor is
+     * at-hand, then run <PRE><CODE>
+     *     morphingTriMesh.addMorph(...);... // anytime before morph()ing
+     *     morphingTriMesh.setMorphInfluencesMap(
+     *             new ListenableStringFloatMap());
+     *     morphingTriMesh.setMorphInfluences(...);
+     *     morphingTriMesh.delegateInfluences();
+     *     morphingTriMesh.morph();
+     * </CODE></PRE>
+     * In this case, the local morphingInfluencesMap that you set up will be
+     * overridden if the following delegateInfluences() call finds a provider.
+     * </P>
+     */
+    public MorphingTriMesh(TriMesh primaryMorph) {
+        super(primaryMorph.getName());
+        this.primaryMorph = primaryMorph;
+        initBase();
+        logger.log(Level.FINE,
+                "Primary morph set for MorphingTriMesh '{0}'", getName());
+    }
+
+    /**
+     * @param morphGeo  Must be a TriMesh instance.
+     * @see #MorphingGeometry#addMorph(String, Geometry)
+     */
+    public void addMorph(String morphKey, Geometry morphGeo) {
+        if (primaryMorph == null)
+            throw new IllegalStateException(
+                    "Primary morph must be set before adding any others");
+        // Validate that compatible with the primary morph.
+        if (!(morphGeo instanceof TriMesh))
+            throw new IllegalArgumentException(
+                    "This class can only handle TriMeshes as morphs");
+        TriMesh morph = (TriMesh) morphGeo;
+        if (morph.getVertexCount() != primaryMorph.getVertexCount())
+            throw new IllegalArgumentException(
+                    "Trimesh " + morph.getName()
+                    + " incompatible with primary Trimesh "
+                    + primaryMorph.getName() + ".  Vertex counts "
+                    + morph.getVertexCount() + " vs. "
+                    + primaryMorph.getVertexCount());
+        if (morph.getMaxIndex() != primaryMorph.getMaxIndex())
+            throw new IllegalArgumentException(
+                    "Trimesh " + morph.getName()
+                    + " incompatible with primary Trimesh "
+                    + primaryMorph.getName() + ".  Max indexes "
+                    + morph.getMaxIndex() + " vs. "
+                    + primaryMorph.getMaxIndex());
+        if ((morph.getNormalBuffer() == null
+                && primaryMorph.getNormalBuffer() != null)
+                || (morph.getNormalBuffer() != null
+                && primaryMorph.getNormalBuffer() == null))
+            throw new IllegalArgumentException(
+                "Normal buffer conflicts with Primary morph");
+
+        enforceEquality("fog coords",
+                primaryMorph.getFogBuffer(), morph.getFogBuffer());
+        enforceEquality("tangent",
+                primaryMorph.getTangentBuffer(), morph.getTangentBuffer());
+        enforceEquality("binormal",
+                primaryMorph.getBinormalBuffer(), morph.getBinormalBuffer());
+
+        dirty = true;
+        morphs.add(morph);
+        morphKeys.add(morphKey);
+        if (morphInfluencesMap != null)
+            morphInfluencesMap.addListener(this, Arrays.asList(morphKey));
+        logger.log(Level.FINE, "Added morph {0} to MorphingTriMesh '{1}':  {2}",
+                new Object[] {
+                morphs.size(), getName(), morph.getName()});
+    }
+
+    protected void enforceEquality(
+            String label, FloatBuffer fb1, FloatBuffer fb2) {
+        if (fb1 == fb2) return;
+        if (fb1 == null || fb2 == null)
+            throw new IllegalArgumentException(
+                    "Incompatible " + label + " values (one is null)");
+        logger.fine("fb1 pre.  Pos/Rem = " + fb1.position() + " / " + fb1.remaining());
+        // TODO:  Remove log statement once verify .equals() does not modify
+        // position or remaining.
+        if (!fb1.equals(fb2))
+            throw new IllegalArgumentException(
+                    "Incompatible " + label + " values");
+        logger.fine("fb1 post.  Pos/Rem = " + fb1.position() + " / " + fb1.remaining());
+        // TODO:  Remove log statement once verify .equals() does not modify
+        // position or remaining.
+    }
+
+    /**
+     * Replaces data other than merge data, by copying from the virgin
+     * primary morph TriMesh.
+     */
+    public void initBase() {
+        if (primaryMorph == null)
+            throw new IllegalStateException(
+                    "Can't initBase when no Geometry has been assigned");
+        logger.fine("Initializing base...");
+        dirty = true;
+        TriMesh primary = primaryMorph; // Just for brevity below
+        RenderState renderState;
+
+        // Set scalars according to primary morph
+        setName(primary.getName());
+        setMode(primary.getMode());
+        setDefaultColor(primary.getDefaultColor());
+        setLightState(primary.getLightState());
+        setCastsShadows(primary.isCastsShadows());
+        for (Controller c : primary.getControllers()) addController(c);
+        setLocalTranslation(new Vector3f(primary.getLocalTranslation()));
+        setLocalScale(new Vector3f(primary.getLocalScale()));
+        setLocalRotation(new Quaternion(primary.getLocalRotation()));
+        setZOrder(primary.getZOrder(), false);
+        setCullHint(primary.getLocalCullHint());
+        setTextureCombineMode(primary.getLocalTextureCombineMode());
+        setLightCombineMode(primary.getLocalLightCombineMode());
+        setRenderQueueMode(primary.getRenderQueueMode());
+        setNormalsMode(primary.getLocalNormalsMode());
+        setIsCollidable(primary.isCollidable());
+        setRenderQueueMode(primary.getLocalRenderQueueMode());
+        for (RenderState.StateType rsType : RenderState.StateType.values()) {
+            clearRenderState(rsType);
+            renderState = primary.getRenderState(rsType);
+            if (renderState != null) setRenderState(renderState);
+        }
+        setIndexBuffer(primaryMorph.getIndexBuffer());
+        setTextureCoords(primaryMorph.getTextureCoords());
+        setColorBuffer(primaryMorph.getColorBuffer());
+        setVBOInfo(primaryMorph.getVBOInfo());
+
+        setLocks(primary.getLocks());
+        logger.info("Base initialized successfully");
+    }
+
+    /**
+     * @see #MorphingGeometry#morph()
+     */
+    public void morph() {
+        if (dirty) forceMorph();
+    }
+
+    /**
+     * @see #MorphingGeometry#forceMorph()
+     */
+    public void forceMorph() {
+        try {
+        if (morphs.size() != morphKeys.size())
+            throw new AssertionError(
+                    "Morph dimensions != morph keys:  "
+                    + morphs.size() + " vs. " + morphKeys.size());
+        List<FloatBuffer> vertBuffers = new ArrayList<FloatBuffer>();
+        List<FloatBuffer> normBuffers = new ArrayList<FloatBuffer>();
+        float[] infs = new float[morphKeys.size()];
+        Float tmpF;
+        if (morphInfluencesMap == null)
+            throw new IllegalStateException(
+                    "morphInfluencesMap must be non-null");
+        for (int i = 0; i < morphKeys.size(); i++) {
+            tmpF = morphInfluencesMap.get(morphKeys.get(i));
+            if (tmpF == null)
+                throw new IllegalStateException(
+                        "Morph influence not set for required key: "
+                        + morphKeys.get(i));
+            infs[i] = tmpF.floatValue();
+        }
+        for (TriMesh morph : morphs) {
+            vertBuffers.add(morph.getVertexBuffer());
+            normBuffers.add(morph.getNormalBuffer());
+            // If more buffers need to be merged, add them here
+        }
+        logger.log(Level.INFO, "Morphing ''{0}'' with influences:  {1}",
+                new String[] {getName(), Arrays.toString(infs)});
+
+        setVertexBuffer(mergeBuffers(
+                primaryMorph.getVertexBuffer(), vertBuffers, infs));
+        setNormalBuffer(mergeBuffers(
+                primaryMorph.getNormalBuffer(), normBuffers, infs));
+
+        //setModelBound(primary.getModelBound()); // Regenereate a BV
+        setHasDirtyVertices(true);  // necessary?
+        // Need to scaleTextureCoordinates()?
+        dirty = false;
+        } catch (RuntimeException re) {
+            if (autoMorph) {
+                autoMorph = false;
+                logger.warning("autoMorphing disabled.  "
+                        + "Turn it back on after you fix your problem");
+                throw re;
+            }
+        }
+        SkinNode skinNode = getSkinNode();
+        if (getSkinNode() != null) skinNode.regenInfluenceOffsets(this);
+    }
+
+    /**
+     * @see MorphingGeometry#getSkinNode()
+     */
+    public SkinNode getSkinNode() {
+        return (getParent() != null && getParent().getParent() != null
+                && (getParent().getParent() instanceof SkinNode)
+                && ((SkinNode) getParent().getParent()).hasSkinGeometry(
+                        getName(), null))
+                ? ((SkinNode) getParent().getParent()) : null;
+    }
+
+    /**
+     * Assumes that influence.length == morphBuffers.size() - 1
+     *
+     * @return null if all the input buffers are null.
+     */
+    protected FloatBuffer mergeBuffers(FloatBuffer primaryBuffer,
+            List<FloatBuffer> morphBuffers, float[] influences) {
+        for (int i = 0; i < morphBuffers.size(); i++) {
+            if (primaryBuffer == null && morphBuffers.get(i) != null)
+                throw new IllegalStateException("Buffer mismatch (A)");
+            if (primaryBuffer != null && morphBuffers.get(i) == null)
+                throw new IllegalStateException("Buffer mismatch (B)");
+        }
+        if (primaryBuffer == null) return null;
+        FloatBuffer outBuffer =
+                BufferUtils.createFloatBuffer(primaryBuffer.capacity());
+        float f0, f;
+        while (outBuffer.hasRemaining()) {
+            f = f0 = primaryBuffer.get();
+            for (int i = 0; i < morphBuffers.size(); i++)
+                f += influences[i] * (morphBuffers.get(i).get() - f0);
+            outBuffer.put(f);
+        }
+        primaryBuffer.flip();
+        for (FloatBuffer fb : morphBuffers) fb.flip();
+        outBuffer.flip();
+        return outBuffer;
+    }
+
+    public void write(JMEExporter e) throws IOException {
+        super.write(e);
+        OutputCapsule capsule = e.getCapsule(this);
+        capsule.writeSavableArrayList(
+                new ArrayList(morphs), "morphs", null);
+        capsule.write(morphKeys.toArray(new String[0]), "morphKeys", null);
+        capsule.write(primaryMorph, "primaryMorph", null);
+        capsule.write(localMorphInfluencesMap, "morphInfluences", null);
+    }
+
+    public void read(JMEImporter e) throws IOException {
+        super.read(e);
+        InputCapsule capsule = e.getCapsule(this);
+        morphs = capsule.readSavableArrayList("morphs", null);
+        String[] morphKeysArray = capsule.readStringArray("morphKeys", null);
+        if (morphKeysArray != null) morphKeys = Arrays.asList(morphKeysArray);
+        primaryMorph = (TriMesh) capsule.readSavable("primaryMorph", null);
+        if (getVertexBuffer() == null) initBase();
+            // A good clue that the MorphingTriMesh needs to be initialized.
+        setMorphInfluencesMap((ListenableStringFloatMap)
+                capsule.readSavable("morphInfluences", null));
+        if (morphInfluencesMap != null) forceMorph();
+        // If want to override the local map with delegation, will have to
+        // manually change the map then re-morph.
+        autoMorph = true;
+    }
+
+    public void floatChanged(StringFloatMap sfm) {
+        dirty = true;
+    }
+
+    public void draw(Renderer r) {
+        //if (dirty) System.err.print('d'); // TODO: REMOVE BEFORE PRODUCTION
+        if (!dirty) super.draw(r);
+    }
+
+    /**
+     * @see #MorphingGeometry#setAutoMorph(boolean)
+     */
+    public void setAutoMorph(boolean autoMorph) {
+        this.autoMorph = autoMorph;
+    }
+
+    protected boolean autoMorph;
+
+    public void updateGeometricState(float time, boolean initiator) {
+        if (autoMorph && dirty) forceMorph();
+        super.updateGeometricState(time, initiator);
+    }
+
+    /**
+     * This behavior is pretty invasive.
+     * <P>
+     * Trying to settle on a behavior which is very automated in normal
+     * circumstances, but allows developers to override everything in a
+     * consistent and reliable way.
+     * </P>
+     *
+     * <b>Behavior here is subject to change</b>.
+    public void setParent(Node parent) {
+        super.setParent(parent);
+        // Make an attempt to set up an influences map if we lack one.
+        if (morphInfluencesMap != null) return;
+        delegateInfluences();
+        if (morphInfluencesMap != null) autoMorph = true;
+    }
+     */
+}