Przeglądaj źródła

Adds a cpu fallback for morph animation when all morph targets cannot be handled on the gpu

Nehon 7 lat temu
rodzic
commit
4048f8ba26

+ 161 - 20
jme3-core/src/main/java/com/jme3/anim/MorphControl.java

@@ -9,7 +9,9 @@ import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.mesh.MorphTarget;
 import com.jme3.shader.VarType;
+import com.jme3.util.BufferUtils;
 import com.jme3.util.SafeArrayList;
+import javafx.geometry.Pos;
 
 import java.nio.FloatBuffer;
 
@@ -17,6 +19,7 @@ import java.nio.FloatBuffer;
  * A control that handle morph animation for Position, Normal and Tangent buffers.
  * All stock shaders only support morphing these 3 buffers, but note that MorphTargets can have any type of buffers.
  * If you want to use other types of buffers you will need a custom MorphControl and a custom shader.
+ *
  * @author Rémy Bouquet
  */
 public class MorphControl extends AbstractControl {
@@ -30,8 +33,17 @@ public class MorphControl extends AbstractControl {
     private boolean approximateTangents = true;
     private MatParamOverride nullNumberOfBones = new MatParamOverride(VarType.Int, "NumberOfBones", null);
 
+    private float[] tmpPosArray;
+    private float[] tmpNormArray;
+    private float[] tmpTanArray;
+
+    private static final VertexBuffer.Type bufferTypes[] = VertexBuffer.Type.values();
+
     @Override
     protected void controlUpdate(float tpf) {
+        if (!enabled) {
+            return;
+        }
         // gathering geometries in the sub graph.
         // This must be done in the update phase as the gathering might add a matparam override
         targets.clear();
@@ -40,15 +52,18 @@ public class MorphControl extends AbstractControl {
 
     @Override
     protected void controlRender(RenderManager rm, ViewPort vp) {
+        if (!enabled) {
+            return;
+        }
         for (Geometry target : targets) {
             Mesh mesh = target.getMesh();
-            if (!mesh.isDirtyMorph()) {
+            if (!target.isDirtyMorph()) {
                 continue;
             }
             int nbMaxBuffers = getRemainingBuffers(mesh, rm.getRenderer());
             Material m = target.getMaterial();
 
-            float weights[] = mesh.getMorphState();
+            float weights[] = target.getMorphState();
             MorphTarget morphTargets[] = mesh.getMorphTargets();
             float matWeights[];
             MatParam param = m.getParam("MorphWeights");
@@ -69,45 +84,171 @@ public class MorphControl extends AbstractControl {
             m.setInt("NumberOfTargetsBuffers", targetNumBuffers);
 
             int nbGPUTargets = 0;
-            int nbCPUBuffers = 0;
+            int lastGpuTargetIndex = 0;
             int boundBufferIdx = 0;
+            float cpuWeightSum = 0;
+            // binding the morphTargets buffer to the mesh morph buffers
             for (int i = 0; i < morphTargets.length; i++) {
+                // discard weights below the threshold
                 if (weights[i] < MIN_WEIGHT) {
                     continue;
                 }
                 if (nbGPUTargets >= maxGPUTargets) {
-                    //TODO we should fallback to CPU there.
-                    nbCPUBuffers++;
+                    // we already bound all the available gpu slots we need to merge the remaining morph targets.
+                    cpuWeightSum += weights[i];
                     continue;
                 }
-                int start = VertexBuffer.Type.MorphTarget0.ordinal();
+                lastGpuTargetIndex = i;
+                // binding the morph target's buffers to the mesh morph buffers.
                 MorphTarget t = morphTargets[i];
-                if (targetNumBuffers >= 1) {
-                    activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Position));
-                    boundBufferIdx++;
-                }
-                if (targetNumBuffers >= 2) {
-                    activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Normal));
-                    boundBufferIdx++;
-                }
-                if (!approximateTangents && targetNumBuffers == 3) {
-                    activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Tangent));
-                    boundBufferIdx++;
-                }
+                boundBufferIdx = bindMorphtargetBuffer(mesh, targetNumBuffers, boundBufferIdx, t);
+                // setting the weight in the mat param array
                 matWeights[nbGPUTargets] = weights[i];
                 nbGPUTargets++;
-
             }
+
             if (nbGPUTargets < matWeights.length) {
+                // if we have less simultaneous GPU targets than the length of the weight array, the array is padded with 0
                 for (int i = nbGPUTargets; i < matWeights.length; i++) {
                     matWeights[i] = 0;
                 }
+            } else if (cpuWeightSum > 0) {
+                // we have more simultaneous morph targets than available gpu slots,
+                // we merge the additional morph targets and bind them to the last gpu slot
+                MorphTarget mt = target.getFallbackMorphTarget();
+                if (mt == null) {
+                    mt = initCpuMorphTarget(target);
+                    target.setFallbackMorphTarget(mt);
+                }
+                // adding the last Gpu target weight
+                cpuWeightSum += matWeights[nbGPUTargets - 1];
+                ensureTmpArraysCapacity(target.getVertexCount() * 3, targetNumBuffers);
+
+                // merging all remaining targets in tmp arrays
+                for (int i = lastGpuTargetIndex; i < morphTargets.length; i++) {
+                    if (weights[i] < MIN_WEIGHT) {
+                        continue;
+                    }
+                    float weight = weights[i] / cpuWeightSum;
+                    MorphTarget t = target.getMesh().getMorphTargets()[i];
+                    mergeMorphTargets(targetNumBuffers, weight, t, i == lastGpuTargetIndex);
+                }
+
+                // writing the tmp arrays to the float buffer
+                writeCpuBuffer(targetNumBuffers, mt);
+
+                // binding the merged morph target
+                bindMorphtargetBuffer(mesh, targetNumBuffers, (nbGPUTargets - 1) * targetNumBuffers, mt);
+
+                // setting the eight of the merged targets
+                matWeights[nbGPUTargets - 1] = cpuWeightSum;
             }
         }
     }
 
+    private int bindMorphtargetBuffer(Mesh mesh, int targetNumBuffers, int boundBufferIdx, MorphTarget t) {
+        int start = VertexBuffer.Type.MorphTarget0.ordinal();
+        if (targetNumBuffers >= 1) {
+            activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Position));
+            boundBufferIdx++;
+        }
+        if (targetNumBuffers >= 2) {
+            activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Normal));
+            boundBufferIdx++;
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Tangent));
+            boundBufferIdx++;
+        }
+        return boundBufferIdx;
+    }
+
+    private void writeCpuBuffer(int targetNumBuffers, MorphTarget mt) {
+        if (targetNumBuffers >= 1) {
+            FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Position);
+            dest.rewind();
+            dest.put(tmpPosArray, 0, dest.capacity());
+        }
+        if (targetNumBuffers >= 2) {
+            FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Normal);
+            dest.rewind();
+            dest.put(tmpNormArray, 0, dest.capacity());
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Tangent);
+            dest.rewind();
+            dest.put(tmpTanArray, 0, dest.capacity());
+        }
+    }
+
+    private void mergeMorphTargets(int targetNumBuffers, float weight, MorphTarget t, boolean init) {
+        if (targetNumBuffers >= 1) {
+            mergeTargetBuffer(tmpPosArray, weight, t.getBuffer(VertexBuffer.Type.Position), init);
+        }
+        if (targetNumBuffers >= 2) {
+            mergeTargetBuffer(tmpNormArray, weight, t.getBuffer(VertexBuffer.Type.Normal), init);
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            mergeTargetBuffer(tmpTanArray, weight, t.getBuffer(VertexBuffer.Type.Tangent), init);
+        }
+    }
+
+    private void ensureTmpArraysCapacity(int capacity, int targetNumBuffers) {
+        if (targetNumBuffers >= 1) {
+            tmpPosArray = ensureCapacity(tmpPosArray, capacity);
+        }
+        if (targetNumBuffers >= 2) {
+            tmpNormArray = ensureCapacity(tmpNormArray, capacity);
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            tmpTanArray = ensureCapacity(tmpTanArray, capacity);
+        }
+    }
+
+    private void mergeTargetBuffer(float[] array, float weight, FloatBuffer src, boolean init) {
+        src.rewind();
+        for (int j = 0; j < src.capacity(); j++) {
+            if (init) {
+                array[j] = 0;
+            }
+            array[j] += weight * src.get();
+        }
+    }
+
     private void activateBuffer(Mesh mesh, int idx, int start, FloatBuffer b) {
-        mesh.setBuffer(VertexBuffer.Type.values()[start + idx], 3, b);
+        VertexBuffer.Type t = bufferTypes[start + idx];
+        VertexBuffer vb = mesh.getBuffer(t);
+        // only set the buffer if it's different
+        if (vb == null || vb.getData() != b) {
+            mesh.setBuffer(t, 3, b);
+        }
+    }
+
+    private float[] ensureCapacity(float[] tmpArray, int size) {
+        if (tmpArray == null || tmpArray.length < size) {
+            return new float[size];
+        }
+        return tmpArray;
+    }
+
+    private MorphTarget initCpuMorphTarget(Geometry geom) {
+        MorphTarget res = new MorphTarget();
+        MorphTarget mt = geom.getMesh().getMorphTargets()[0];
+        FloatBuffer b = mt.getBuffer(VertexBuffer.Type.Position);
+        if (b != null) {
+            res.setBuffer(VertexBuffer.Type.Position, BufferUtils.createFloatBuffer(b.capacity()));
+        }
+        b = mt.getBuffer(VertexBuffer.Type.Normal);
+        if (b != null) {
+            res.setBuffer(VertexBuffer.Type.Normal, BufferUtils.createFloatBuffer(b.capacity()));
+        }
+        if (!approximateTangents) {
+            b = mt.getBuffer(VertexBuffer.Type.Tangent);
+            if (b != null) {
+                res.setBuffer(VertexBuffer.Type.Tangent, BufferUtils.createFloatBuffer(b.capacity()));
+            }
+        }
+        return res;
     }
 
     private int getTargetNumBuffers(MorphTarget morphTarget) {

+ 2 - 3
jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java

@@ -45,10 +45,9 @@ public class ClipAction extends BlendableAction {
     }
     private void interpolateMorphTrack(double t, MorphTrack track) {
         Geometry target = track.getTarget();
-        float[] weights = new float[target.getMesh().getMorphTargets().length];
+        float[] weights = target.getMorphState();
         track.getDataAtTime(t, weights);
-        target.getMesh().setMorphState(weights);
-
+        target.setMorphState(weights);
 
 //        if (collectTransformDelegate != null) {
 //            collectTransformDelegate.collectTransform(target, transform, getWeight(), this);

+ 43 - 0
jme3-core/src/main/java/com/jme3/scene/Geometry.java

@@ -43,6 +43,7 @@ import com.jme3.material.Material;
 import com.jme3.math.Matrix4f;
 import com.jme3.renderer.Camera;
 import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.mesh.MorphTarget;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.IdentityCloneFunction;
@@ -86,6 +87,15 @@ public class Geometry extends Spatial {
      */
     protected int startIndex = -1;
 
+    /**
+     * Morph state variable for morph animation
+     */
+    private float[] morphState;
+    private boolean dirtyMorph = true;
+    // a Morph target that will be used to merge all targets that
+    // can't be handled on the cpu on each frame.
+    private MorphTarget fallbackMorphTarget;
+
     /**
      * Serialization only. Do not use.
      */
@@ -576,6 +586,39 @@ public class Geometry extends Spatial {
         this.material = cloner.clone(material);
     }
 
+    public void setMorphState(float[] state) {
+        if (mesh == null || mesh.getMorphTargets().length == 0){
+            return;
+        }
+
+        int nbMorphTargets = mesh.getMorphTargets().length;
+
+        if (morphState == null) {
+            morphState = new float[nbMorphTargets];
+        }
+        System.arraycopy(state, 0, morphState, 0, morphState.length);
+        this.dirtyMorph = true;
+    }
+
+    public boolean isDirtyMorph() {
+        return dirtyMorph;
+    }
+
+    public float[] getMorphState() {
+        if (morphState == null) {
+            morphState = new float[mesh.getMorphTargets().length];
+        }
+        return morphState;
+    }
+
+    public MorphTarget getFallbackMorphTarget() {
+        return fallbackMorphTarget;
+    }
+
+    public void setFallbackMorphTarget(MorphTarget fallbackMorphTarget) {
+        this.fallbackMorphTarget = fallbackMorphTarget;
+    }
+
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);

+ 6 - 57
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -185,9 +185,6 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
     private Mode mode = Mode.Triangles;
 
     private SafeArrayList<MorphTarget> morphTargets;
-    private int numMorphBuffers = 0;
-    private float[] morphState;
-    private boolean dirtyMorph = true;
 
     /**
      * Creates a new mesh with no {@link VertexBuffer vertex buffers}.
@@ -1527,52 +1524,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         if (morphTargets == null) {
             morphTargets = new SafeArrayList<>(MorphTarget.class);
         }
-//        if (numMorphBuffers == 0) {
-//            numMorphBuffers = target.getNumBuffers();
-//            int start = Type.MorphTarget0.ordinal();
-//            int end = start + numMorphBuffers;
-//            for (int i = start; i < end; i++) {
-//                VertexBuffer vb = new VertexBuffer(Type.values()[i]);
-//                setBuffer(vb);
-//            }
-//        } else if (target.getNumBuffers() != numMorphBuffers) {
-//            throw new IllegalArgumentException("Morph target has different number of buffers");
-//        }
-
         morphTargets.add(target);
     }
 
-    public void setMorphState(float[] state) {
-        if (morphTargets.isEmpty()) {
-            return;
-        }
-        if (morphState == null) {
-            morphState = new float[morphTargets.size()];
-        }
-        System.arraycopy(state, 0, morphState, 0, morphState.length);
-        this.dirtyMorph = true;
-    }
-
-    public float[] getMorphState() {
-        if (morphState == null) {
-            morphState = new float[morphTargets.size()];
-        }
-        return morphState;
-    }
-
-    public void setActiveMorphTargets(int... targetsIndex) {
-        int start = Type.MorphTarget0.ordinal();
-        for (int i = 0; i < targetsIndex.length; i++) {
-            MorphTarget t = morphTargets.get(targetsIndex[i]);
-            int idx = 0;
-            for (Type type : t.getBuffers().keySet()) {
-                FloatBuffer b = t.getBuffer(type);
-                setBuffer(Type.values()[start + i + idx], 3, b);
-                idx++;
-            }
-        }
-    }
-
     public MorphTarget[] getMorphTargets() {
         return morphTargets.getArray();
     }
@@ -1581,21 +1535,10 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         return morphTargets != null && !morphTargets.isEmpty();
     }
 
-    public boolean isDirtyMorph() {
-        return dirtyMorph;
-    }
-
     @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule out = ex.getCapsule(this);
 
-//        HashMap<String, VertexBuffer> map = new HashMap<String, VertexBuffer>();
-//        for (Entry<VertexBuffer> buf : buffers){
-//            if (buf.getValue() != null)
-//                map.put(buf.getKey()+"a", buf.getValue());
-//        }
-//        out.writeStringSavableMap(map, "buffers", null);
-
         out.write(meshBound, "modelBound", null);
         out.write(vertCount, "vertCount", -1);
         out.write(elementCount, "elementCount", -1);
@@ -1630,6 +1573,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         }
 
         out.write(lodLevels, "lodLevels", null);
+        out.writeSavableArrayList(new ArrayList(morphTargets), "morphTargets", null);
     }
 
     @Override
@@ -1669,6 +1613,11 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             lodLevels = new VertexBuffer[lodLevelsSavable.length];
             System.arraycopy( lodLevelsSavable, 0, lodLevels, 0, lodLevels.length);
         }
+
+        ArrayList<Savable> l = in.readSavableArrayList("morphTargets", null);
+        if (l != null) {
+            morphTargets = new SafeArrayList(MorphTarget.class, l);
+        }
     }
 
 }

+ 15 - 4
jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java

@@ -1,13 +1,13 @@
 package com.jme3.scene.mesh;
 
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
 import com.jme3.scene.VertexBuffer;
 
 import java.io.IOException;
+import java.nio.Buffer;
 import java.nio.FloatBuffer;
 import java.util.EnumMap;
+import java.util.Map;
 
 public class MorphTarget implements Savable {
     private EnumMap<VertexBuffer.Type, FloatBuffer> buffers = new EnumMap<>(VertexBuffer.Type.class);
@@ -30,11 +30,22 @@ public class MorphTarget implements Savable {
 
     @Override
     public void write(JmeExporter ex) throws IOException {
-
+        OutputCapsule oc = ex.getCapsule(this);
+        for (Map.Entry<VertexBuffer.Type, FloatBuffer> entry : buffers.entrySet()) {
+            Buffer roData = entry.getValue().asReadOnlyBuffer();
+            oc.write((FloatBuffer) roData, entry.getKey().name(),null);
+        }
     }
 
     @Override
     public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        for (VertexBuffer.Type type : VertexBuffer.Type.values()) {
+            FloatBuffer b = ic.readFloatBuffer(type.name(), null);
+            if(b!= null){
+                setBuffer(type, b);
+            }
+        }
 
     }
 }

+ 4 - 13
jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java

@@ -33,8 +33,7 @@ package jme3test.model;
 
 import com.jme3.anim.AnimComposer;
 import com.jme3.anim.SkinningControl;
-import com.jme3.app.ChaseCameraAppState;
-import com.jme3.app.SimpleApplication;
+import com.jme3.app.*;
 import com.jme3.asset.plugins.FileLocator;
 import com.jme3.input.KeyInput;
 import com.jme3.input.controls.ActionListener;
@@ -45,7 +44,6 @@ import com.jme3.scene.*;
 import com.jme3.scene.control.Control;
 import com.jme3.scene.debug.custom.ArmatureDebugAppState;
 import com.jme3.scene.plugins.gltf.GltfModelKey;
-import com.jme3.shader.VarType;
 import jme3test.model.anim.EraseTimer;
 
 import java.util.*;
@@ -118,8 +116,8 @@ public class TestGltfLoading extends SimpleApplication {
 //        rootNode.addLight(pl1);
 
         //loadModel("Models/gltf/polly/project_polly.gltf", new Vector3f(0, 0, 0), 0.5f);
-        //loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.1f);
-        loadModel("Models/gltf/scifigirl/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
+        loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.1f);
+    //    loadModel("Models/gltf/scifigirl/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
         //loadModel("Models/gltf/man/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
        //loadModel("Models/gltf/torus/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
         //loadModel("Models/gltf/morph/scene.gltf", new Vector3f(0, 0, 0), 0.2f);
@@ -214,15 +212,8 @@ public class TestGltfLoading extends SimpleApplication {
         }, "nextAnim");
 
         dumpScene(rootNode, 0);
-    }
 
-    public void setMorphTarget(int index) {
-        g = (Geometry) probeNode.getChild("0");
-        g.getMesh().setActiveMorphTargets(index);
-        g.getMaterial().setInt("NumberOfMorphTargets", 1);
-        g.getMaterial().setInt("NumberOfTargetsBuffers", 3);
-        float[] weights = {1.0f};
-        g.getMaterial().setParam("MorphWeights", VarType.FloatArray, weights);
+        stateManager.attach(new DetailedProfilerState());
     }
 
     private <T extends Control> T findControl(Spatial s, Class<T> controlClass) {

+ 168 - 0
jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java

@@ -0,0 +1,168 @@
+package jme3test.model.anim;
+
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.SkinningControl;
+import com.jme3.anim.util.AnimMigrationUtils;
+import com.jme3.app.ChaseCameraAppState;
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.plugins.FileLocator;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.math.*;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.debug.custom.ArmatureDebugAppState;
+import com.jme3.system.JmeSystem;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Created by Nehon on 18/12/2017.
+ */
+public class TestAnimSerialization extends SimpleApplication {
+
+    ArmatureDebugAppState debugAppState;
+    AnimComposer composer;
+    Queue<String> anims = new LinkedList<>();
+    boolean playAnim = true;
+    File file;
+
+    public static void main(String... argv) {
+        TestAnimSerialization app = new TestAnimSerialization();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        setTimer(new EraseTimer());
+        //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f);
+        viewPort.setBackgroundColor(ColorRGBA.DarkGray);
+        rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal()));
+        rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray));
+
+        Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o");
+
+        AnimMigrationUtils.migrate(model);
+
+        File storageFolder = JmeSystem.getStorageFolder();
+        file = new File(storageFolder.getPath() + File.separator + "newJaime.j3o");
+        BinaryExporter be = new BinaryExporter();
+        try {
+            be.save(model, file);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        assetManager.registerLocator(storageFolder.getPath(), FileLocator.class);
+        model = assetManager.loadModel("newJaime.j3o");
+
+        rootNode.attachChild(model);
+
+        debugAppState = new ArmatureDebugAppState();
+        stateManager.attach(debugAppState);
+
+        setupModel(model);
+
+        flyCam.setEnabled(false);
+
+        Node target = new Node("CamTarget");
+        //target.setLocalTransform(model.getLocalTransform());
+        target.move(0, 1, 0);
+        ChaseCameraAppState chaseCam = new ChaseCameraAppState();
+        chaseCam.setTarget(target);
+        getStateManager().attach(chaseCam);
+        chaseCam.setInvertHorizontalAxis(true);
+        chaseCam.setInvertVerticalAxis(true);
+        chaseCam.setZoomSpeed(0.5f);
+        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
+        chaseCam.setRotationSpeed(3);
+        chaseCam.setDefaultDistance(3);
+        chaseCam.setMinDistance(0.01f);
+        chaseCam.setZoomSpeed(0.01f);
+        chaseCam.setDefaultVerticalRotation(0.3f);
+
+        initInputs();
+    }
+
+    public void initInputs() {
+        inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN));
+
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if (isPressed) {
+                    playAnim = !playAnim;
+                    if (playAnim) {
+                        String anim = anims.poll();
+                        anims.add(anim);
+                        composer.setCurrentAction(anim);
+                        System.err.println(anim);
+                    } else {
+                        composer.reset();
+                    }
+                }
+            }
+        }, "toggleAnim");
+        inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT));
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if (isPressed && composer != null) {
+                    String anim = anims.poll();
+                    anims.add(anim);
+                    composer.setCurrentAction(anim);
+                    System.err.println(anim);
+                }
+            }
+        }, "nextAnim");
+    }
+
+    private void setupModel(Spatial model) {
+        if (composer != null) {
+            return;
+        }
+        composer = model.getControl(AnimComposer.class);
+        if (composer != null) {
+
+            SkinningControl sc = model.getControl(SkinningControl.class);
+
+            debugAppState.addArmatureFrom(sc);
+            anims.clear();
+            for (String name : composer.getAnimClipsNames()) {
+                anims.add(name);
+            }
+            if (anims.isEmpty()) {
+                return;
+            }
+            if (playAnim) {
+                String anim = anims.poll();
+                anims.add(anim);
+                composer.setCurrentAction(anim);
+                System.err.println(anim);
+            }
+
+        } else {
+            if (model instanceof Node) {
+                Node n = (Node) model;
+                for (Spatial child : n.getChildren()) {
+                    setupModel(child);
+                }
+            }
+        }
+
+    }
+
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        file.delete();
+    }
+}

+ 6 - 5
jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java

@@ -1,5 +1,6 @@
 package jme3test.model.anim;
 
+import com.jme3.anim.MorphControl;
 import com.jme3.app.ChaseCameraAppState;
 import com.jme3.app.SimpleApplication;
 import com.jme3.input.KeyInput;
@@ -68,16 +69,16 @@ public class TestMorph extends SimpleApplication {
         target2.setBuffer(VertexBuffer.Type.Position, buffer);
         box.addMorphTarget(target2);
 
-        Geometry g = new Geometry("box", box);
-        final Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        final Geometry g = new Geometry("box", box);
+        Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
         g.setMaterial(m);
         m.setColor("Color", ColorRGBA.Red);
         m.setInt("NumberOfMorphTargets", 2);
 
         rootNode.attachChild(g);
 
-        box.setActiveMorphTargets(0,1);
-        m.setParam("MorphWeights", VarType.FloatArray, weights);
+        g.setMorphState(weights);
+        g.addControl(new MorphControl());
 
         ChaseCameraAppState chase = new ChaseCameraAppState();
         chase.setTarget(rootNode);
@@ -104,7 +105,7 @@ public class TestMorph extends SimpleApplication {
                 if (name.equals("morphdown")) {
                     weights[1] -= tpf;
                 }
-                m.setParam("MorphWeights", VarType.FloatArray, weights);
+                g.setMorphState(weights);
 
             }
         }, "morphup", "morphdown", "morphleft", "morphright");

+ 0 - 5
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -803,11 +803,6 @@ public class GltfLoader implements AssetLoader {
             } else {
                 trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Morph));
                 float[] weights = readAccessorData(dataIndex, floatArrayPopulator);
-                Geometry g = fetchFromCache("nodes", targetNode, Geometry.class);
-                int expectedSize = g.getMesh().getMorphTargets().length * times.length;
-                if( expectedSize != weights.length ){
-                    throw new AssetLoadException("Morph animation should contain " + expectedSize + " entries, got" + weights.length);
-                }
                 trackData.weights = weights;
                 hasMorphTrack = true;
             }