Przeglądaj źródła

FBX: new FBX importer (not yet enabled - old importer still used by default)
Still needs work:
* Skeletal animation (many issues with transform hierarchies)
* N-gons triangulation (only quads supported at the moment)
* Light & Camera importing
* Z-up to Y-up correction
* Morph animation

Kirill Vainer 10 lat temu
rodzic
commit
ed2be5e542
34 zmienionych plików z 5357 dodań i 0 usunięć
  1. 413 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/FbxLoader.java
  2. 144 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurve.java
  3. 147 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurveNode.java
  4. 82 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimLayer.java
  5. 111 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimStack.java
  6. 44 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimUtil.java
  7. 103 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxBindPose.java
  8. 98 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxCluster.java
  9. 94 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxLimbNode.java
  10. 66 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxSkinDeformer.java
  11. 202 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxToJmeTrack.java
  12. 190 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxImage.java
  13. 363 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxMaterial.java
  14. 234 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxMaterialProperties.java
  15. 146 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxTexture.java
  16. 107 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxLayer.java
  17. 243 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxLayerElement.java
  18. 316 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java
  19. 69 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMeshUtil.java
  20. 59 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxPolygon.java
  21. 145 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/misc/FbxGlobalSettings.java
  22. 617 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java
  23. 41 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNodeAttribute.java
  24. 61 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNodeUtil.java
  25. 59 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNullAttribute.java
  26. 45 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxRootNode.java
  27. 144 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/obj/FbxObject.java
  28. 209 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/obj/FbxObjectFactory.java
  29. 54 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/obj/FbxUnknownObject.java
  30. 89 0
      jme3-plugins/src/main/java/com/jme3/scene/plugins/IrBoneWeightIndex.java
  31. 46 0
      jme3-plugins/src/main/java/com/jme3/scene/plugins/IrMesh.java
  32. 46 0
      jme3-plugins/src/main/java/com/jme3/scene/plugins/IrPolygon.java
  33. 400 0
      jme3-plugins/src/main/java/com/jme3/scene/plugins/IrUtils.java
  34. 170 0
      jme3-plugins/src/main/java/com/jme3/scene/plugins/IrVertex.java

+ 413 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/FbxLoader.java

@@ -0,0 +1,413 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx;
+
+import com.jme3.animation.AnimControl;
+import com.jme3.animation.Animation;
+import com.jme3.animation.Bone;
+import com.jme3.animation.BoneTrack;
+import com.jme3.animation.Skeleton;
+import com.jme3.animation.Track;
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLoadException;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.ModelKey;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Transform;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.fbx.anim.FbxToJmeTrack;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimCurveNode;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimLayer;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimStack;
+import com.jme3.scene.plugins.fbx.anim.FbxBindPose;
+import com.jme3.scene.plugins.fbx.anim.FbxLimbNode;
+import com.jme3.scene.plugins.fbx.file.FbxDump;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.file.FbxFile;
+import com.jme3.scene.plugins.fbx.file.FbxReader;
+import com.jme3.scene.plugins.fbx.file.FbxId;
+import com.jme3.scene.plugins.fbx.misc.FbxGlobalSettings;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import com.jme3.scene.plugins.fbx.node.FbxRootNode;
+import com.jme3.scene.plugins.fbx.obj.FbxObjectFactory;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxLoader implements AssetLoader {
+    
+    private static final Logger logger = Logger.getLogger(FbxLoader.class.getName());
+    
+    private AssetManager assetManager;
+    
+    private String sceneName;
+    private String sceneFilename;
+    private String sceneFolderName;
+    private FbxGlobalSettings globalSettings;
+    private final Map<FbxId, FbxObject> objectMap = new HashMap<FbxId, FbxObject>();
+    
+    private final List<FbxAnimStack> animStacks = new ArrayList<FbxAnimStack>();
+    private final List<FbxBindPose> bindPoses = new ArrayList<FbxBindPose>();
+    
+    @Override
+    public Object load(AssetInfo assetInfo) throws IOException {
+        this.assetManager = assetInfo.getManager();
+        AssetKey<?> assetKey = assetInfo.getKey();
+        if (!(assetKey instanceof ModelKey)) {
+            throw new AssetLoadException("Invalid asset key");
+        }
+        
+        InputStream stream = assetInfo.openStream();
+        try {
+            sceneFilename = assetKey.getName();
+            sceneFolderName = assetKey.getFolder();
+            String ext = assetKey.getExtension();
+            
+            sceneName = sceneFilename.substring(0, sceneFilename.length() - ext.length() - 1);
+            if (sceneFolderName != null && sceneFolderName.length() > 0) {
+                sceneName = sceneName.substring(sceneFolderName.length());
+            }
+            
+            reset();
+            
+            // Load the data from the stream.
+            loadData(stream);
+            
+            // Bind poses are needed to compute world transforms.
+            applyBindPoses();
+            
+            // Need world transforms for skeleton creation.
+            updateWorldTransforms();
+            
+            // Need skeletons for meshs to be created in scene graph construction.
+            // Mesh bone indices require skeletons to determine bone index.
+            constructSkeletons();
+            
+            // Create the jME3 scene graph from the FBX scene graph.
+            // Also creates SkeletonControls based on the constructed skeletons.
+            Spatial scene = constructSceneGraph();
+            
+            // Load animations into AnimControls
+            constructAnimations();
+           
+            return scene;
+        } finally {
+            releaseObjects();
+            if (stream != null) {
+                stream.close();
+            }
+        }
+    }
+    
+    private void reset() {
+        globalSettings = new FbxGlobalSettings();
+    }
+    
+    private void releaseObjects() {
+        globalSettings = null;
+        objectMap.clear();
+        animStacks.clear();
+    }
+    
+    private void loadData(InputStream stream) throws IOException {
+        FbxFile scene = FbxReader.readFBX(stream);
+
+        FbxDump.dumpFile(scene);
+
+        // TODO: Load FBX object templates
+        
+        for (FbxElement e : scene.rootElements) {
+            if (e.id.equals("FBXHeaderExtension")) {
+                loadHeader(e);
+            } else if (e.id.equals("GlobalSettings")) {
+                loadGlobalSettings(e);
+             } else if (e.id.equals("Objects")) {
+                loadObjects(e);
+            } else if (e.id.equals("Connections")) {
+                connectObjects(e);
+            }
+        }
+    }
+    
+    private void loadHeader(FbxElement element) {
+        for (FbxElement e : element.children) {
+            if (e.id.equals("FBXVersion")) {
+                Integer version = (Integer) e.properties.get(0);
+                if (version < 7100) {
+                    logger.log(Level.WARNING, "FBX file version is older than 7.1. "
+                                            + "Some features may not work.");
+                }
+            }
+        }
+    }
+    
+    private void loadGlobalSettings(FbxElement element) {
+        globalSettings = new FbxGlobalSettings();
+        globalSettings.fromElement(element);
+    }
+    
+    private void loadObjects(FbxElement element) {
+        // Initialize the FBX root element.
+        objectMap.put(FbxId.ROOT, new FbxRootNode(assetManager, sceneFolderName));
+
+        for(FbxElement e : element.children) {
+            if (e.id.equals("GlobalSettings")) {
+                // Old FBX files seem to have the GlobalSettings element
+                // under Objects (??)
+                globalSettings.fromElement(e);
+            } else {
+                FbxObject object = FbxObjectFactory.createObject(e, assetManager, sceneFolderName);
+                if (object != null) {
+                    if (objectMap.containsKey(object.getId())) {
+                        logger.log(Level.WARNING, "An object with ID \"{0}\" has "
+                                                + "already been defined. "
+                                                + "Ignoring.", 
+                                                   object.getId());
+                    }
+                    
+                    objectMap.put(object.getId(), object);
+                    
+                    if (object instanceof FbxAnimStack) {
+                        // NOTE: animation stacks are implicitly global.
+                        // Capture them here.
+                        animStacks.add((FbxAnimStack) object);
+                    } else if (object instanceof FbxBindPose) {
+                        bindPoses.add((FbxBindPose) object);
+                    }
+                } else {
+                    throw new UnsupportedOperationException("Failed to create FBX object of type: " + e.id);
+                }
+            }
+        }
+    }
+    
+    private void removeUnconnectedObjects() {
+        for (FbxObject object : new ArrayList<FbxObject>(objectMap.values())) {
+            if (!object.isJmeObjectCreated()) {
+                logger.log(Level.WARNING, "Purging orphan FBX object: {0}", object);
+                objectMap.remove(object.getId());
+            }
+        }
+    }
+    
+    private void connectObjects(FbxElement element) {
+        if (objectMap.isEmpty()) {
+            logger.log(Level.WARNING, "FBX file is missing object information");
+            return;
+        } else if (objectMap.size() == 1) {
+            // Only root node (automatically added by jME3)
+            logger.log(Level.WARNING, "FBX file has no objects");
+            return;
+        }
+        
+        for (FbxElement el : element.children) {
+            if (!el.id.equals("C") && !el.id.equals("Connect")) {
+                continue;
+            }
+            String type = (String) el.properties.get(0);
+            FbxId childId;
+            FbxId parentId;
+            if (type.equals("OO")) {
+                childId = FbxId.create(el.properties.get(1));
+                parentId = FbxId.create(el.properties.get(2));
+                FbxObject child = objectMap.get(childId);
+                FbxObject parent;
+
+                if (parentId.isNull()) {
+                    // TODO: maybe clean this up a bit..
+                    parent = objectMap.get(FbxId.ROOT);
+                } else {
+                    parent = objectMap.get(parentId);
+                }
+
+                if (parent == null) {
+                    throw new UnsupportedOperationException("Cannot find parent object ID \"" + parentId + "\"");
+                }
+
+                parent.connectObject(child);
+            } else if (type.equals("OP")) {
+                childId = FbxId.create(el.properties.get(1));
+                parentId = FbxId.create(el.properties.get(2));
+                String propName = (String) el.properties.get(3);
+                FbxObject child = objectMap.get(childId);
+                FbxObject parent = objectMap.get(parentId);
+                parent.connectObjectProperty(child, propName);
+            } else {
+                logger.log(Level.WARNING, "Unknown connection type: {0}. Ignoring.", type);
+            }
+        }
+    }
+    
+    /**
+     * Copies the bind poses from FBX BindPose objects to FBX nodes.
+     * Must be called prior to {@link #updateWorldTransforms()}.
+     */
+    private void applyBindPoses() {
+        for (FbxBindPose bindPose : bindPoses) {
+            Map<FbxId, Matrix4f> bindPoseData = bindPose.getJmeObject();
+            logger.log(Level.INFO, "Applying {0} bind poses", bindPoseData.size());
+            for (Map.Entry<FbxId, Matrix4f> entry : bindPoseData.entrySet()) {
+                FbxObject obj = objectMap.get(entry.getKey());
+                if (obj instanceof FbxNode) {
+                    FbxNode node = (FbxNode) obj;
+                    node.setWorldBindPose(entry.getValue());
+                } else {
+                    logger.log(Level.WARNING, "Bind pose can only be applied to FBX nodes. Ignoring.");
+                }
+            }
+        }
+    }
+    
+    /**
+     * Updates world transforms and bind poses for the FBX scene graph.
+     */
+    private void updateWorldTransforms() {
+        FbxNode fbxRoot = (FbxNode) objectMap.get(FbxId.ROOT);
+        fbxRoot.updateWorldTransforms(null, null);
+    }
+    
+    private void constructAnimations() {
+        // In FBX, animation are not attached to any root.
+        // They are implicitly global.
+        // So, we need to use hueristics to find which node(s) 
+        // an animation is associated with, so we can create the AnimControl
+        // in the appropriate location in the scene.
+        Map<FbxToJmeTrack, FbxToJmeTrack> pairs = new HashMap<FbxToJmeTrack, FbxToJmeTrack>();
+        for (FbxAnimStack stack : animStacks) {
+            for (FbxAnimLayer layer : stack.getLayers()) {
+                for (FbxAnimCurveNode curveNode : layer.getAnimationCurveNodes()) {
+                    for (Map.Entry<FbxNode, String> nodePropertyEntry : curveNode.getInfluencedNodeProperties().entrySet()) {
+                        FbxToJmeTrack lookupPair = new FbxToJmeTrack();
+                        lookupPair.animStack = stack;
+                        lookupPair.animLayer = layer;
+                        lookupPair.node = nodePropertyEntry.getKey();
+                        
+                        // Find if this pair is already stored
+                        FbxToJmeTrack storedPair = pairs.get(lookupPair);
+                        if (storedPair == null) {
+                            // If not, store it.
+                            storedPair = lookupPair;
+                            pairs.put(storedPair, storedPair);
+                        }
+                        
+                        String property = nodePropertyEntry.getValue();
+                        storedPair.animCurves.put(property, curveNode);
+                    }
+                }
+            }
+        }
+        
+        // At this point we can construct the animation for all pairs ...
+        for (FbxToJmeTrack pair : pairs.values()) {
+            String animName = pair.animStack.getName();
+            float duration    = pair.animStack.getDuration();
+            
+            System.out.println("ANIMATION: " + animName + ", duration = " + duration);
+            System.out.println("NODE: " + pair.node.getName());
+            
+            duration = pair.getDuration();
+            
+            if (pair.node instanceof FbxLimbNode) {
+                // Find the spatial that has the skeleton for this limb.
+                FbxLimbNode limbNode = (FbxLimbNode) pair.node;
+                Bone bone = limbNode.getJmeBone();
+                Spatial jmeSpatial = limbNode.getSkeletonHolder().getJmeObject();
+                Skeleton skeleton = limbNode.getSkeletonHolder().getJmeSkeleton();
+                
+                // Get the animation control (create if missing).
+                AnimControl animControl = jmeSpatial.getControl(AnimControl.class);
+                if (animControl.getSkeleton() != skeleton) {
+                    throw new UnsupportedOperationException();
+                }
+                
+                // Get the animation (create if missing).
+                Animation anim = animControl.getAnim(animName);
+                if (anim == null) { 
+                    anim = new Animation(animName, duration);
+                    animControl.addAnim(anim);
+                }
+                
+                // Find the bone index from the spatial's skeleton.
+                int boneIndex = skeleton.getBoneIndex(bone);
+                
+                // Generate the bone track.
+                BoneTrack bt = pair.toJmeBoneTrack(boneIndex, bone.getBindInverseTransform());
+                
+                // Add the bone track to the animation.
+                anim.addTrack(bt);
+            } else {
+                // Create the spatial animation
+                Animation anim = new Animation(animName, duration);
+                anim.setTracks(new Track[]{ pair.toJmeSpatialTrack() });
+
+                // Get the animation control (create if missing).
+                Spatial jmeSpatial = pair.node.getJmeObject();
+                AnimControl animControl = jmeSpatial.getControl(AnimControl.class);
+                
+                if (animControl == null) { 
+                    animControl = new AnimControl(null);
+                    jmeSpatial.addControl(animControl);
+                }
+                
+                // Add the spatial animation
+                animControl.addAnim(anim);
+            }
+        }
+    }
+    
+    private void constructSkeletons() {
+        FbxNode fbxRoot = (FbxNode) objectMap.get(FbxId.ROOT);
+        FbxNode.createSkeletons(fbxRoot);
+    }
+    
+    private Spatial constructSceneGraph() {
+        // Acquire the implicit root object.
+        FbxNode fbxRoot = (FbxNode) objectMap.get(FbxId.ROOT);
+
+        // Convert it into a jME3 scene
+        Node jmeRoot = (Node) FbxNode.createScene(fbxRoot);
+
+        // Fix the name (will probably be set to something like "-node")
+        jmeRoot.setName(sceneName + "-scene");
+
+        return jmeRoot;
+    }
+}

+ 144 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurve.java

@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.math.FastMath;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+
+public class FbxAnimCurve extends FbxObject {
+
+    private long[] keyTimes;
+    private float[] keyValues;
+    
+    public FbxAnimCurve(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        
+        for (FbxElement e : element.children) {
+            if (e.id.equals("KeyTime")) {
+                keyTimes = (long[]) e.properties.get(0);
+            } else if (e.id.equals("KeyValueFloat")) {
+                keyValues = (float[]) e.properties.get(0);
+            }
+        }
+        
+        long time = -1;
+        for (int i = 0; i < keyTimes.length; i++) {
+            if (time >= keyTimes[i]) {
+                throw new UnsupportedOperationException("Keyframe times must be sequential, but they are not.");
+            }
+            time = keyTimes[i];
+        }
+    }
+
+    /**
+     * Get the times for the keyframes.
+     * @return Keyframe times. 
+     */
+    public long[] getKeyTimes() {
+        return keyTimes;
+    }
+    
+    /**
+     * Retrieve the curve value at the given time.
+     * If the curve has no data, 0 is returned.
+     * If the time is outside the curve, then the closest value is returned.
+     * If the time isn't on an exact keyframe, linear interpolation is used
+     * to determine the value between the keyframes at the given time.
+     * @param time The time to get the curve value at (in FBX time units).
+     * @return The value at the given time.
+     */
+    public float getValueAtTime(long time) {
+        if (keyTimes.length == 0) {
+            return 0;
+        }
+        
+        // If the time is outside the range, 
+        // we just return the closest value. (No extrapolation)
+        if (time <= keyTimes[0]) {
+            return keyValues[0];
+        } else if (time >= keyTimes[keyTimes.length - 1]) {
+            return keyValues[keyValues.length - 1];
+        }
+        
+        
+
+        int startFrame = 0;
+        int endFrame = 1;
+        int lastFrame = keyTimes.length - 1;
+        
+        for (int i = 0; i < lastFrame && keyTimes[i] < time; ++i) {
+            startFrame = i;
+            endFrame = i + 1;
+        }
+        
+        long keyTime1    = keyTimes[startFrame];
+        float keyValue1  = keyValues[startFrame];
+        long keyTime2    = keyTimes[endFrame];
+        float keyValue2  = keyValues[endFrame];
+        
+        if (keyTime2 == time) {
+            return keyValue2;
+        }
+        
+        long prevToNextDelta    = keyTime2 - keyTime1;
+        long prevToCurrentDelta = time     - keyTime1;
+        float lerpAmount = (float)prevToCurrentDelta / prevToNextDelta;
+        
+        return FastMath.interpolateLinear(lerpAmount, keyValue1, keyValue2);
+    }
+
+    @Override
+    protected Object toJmeObject() {
+        // An AnimCurve has no jME3 representation.
+        // The parent AnimCurveNode is responsible to create the jME3 
+        // representation.
+        throw new UnsupportedOperationException("No jME3 object conversion available");
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+}

+ 147 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurveNode.java

@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxAnimCurveNode extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxAnimCurveNode.class.getName());
+    
+    private final Map<FbxNode, String> influencedNodePropertiesMap = new HashMap<FbxNode, String>();
+    private final Map<String, FbxAnimCurve> propertyToCurveMap = new HashMap<String, FbxAnimCurve>();
+    private final Map<String, Float> propertyToDefaultMap = new HashMap<String, Float>();
+    
+    public FbxAnimCurveNode(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement prop : element.getFbxProperties()) {
+            String propName = (String) prop.properties.get(0);
+            String propType = (String) prop.properties.get(1);
+            if (propType.equals("Number")) {
+                float propValue = ((Double) prop.properties.get(4)).floatValue();
+                propertyToDefaultMap.put(propName, propValue);
+            }
+        }
+    }
+    
+    public void addInfluencedNode(FbxNode node, String property) {
+        influencedNodePropertiesMap.put(node, property);
+    }
+    
+    public Map<FbxNode, String> getInfluencedNodeProperties() {
+        return influencedNodePropertiesMap;
+    }
+    
+    public Collection<FbxAnimCurve> getCurves() {
+        return propertyToCurveMap.values();
+    }
+    
+    public Vector3f getVector3Value(long time) {
+        Vector3f value = new Vector3f();
+        FbxAnimCurve xCurve = propertyToCurveMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_X);
+        FbxAnimCurve yCurve = propertyToCurveMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Y);
+        FbxAnimCurve zCurve = propertyToCurveMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Z);
+        Float xDefault      = propertyToDefaultMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_X);
+        Float yDefault      = propertyToDefaultMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Y);
+        Float zDefault      = propertyToDefaultMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Z);
+        value.x = xCurve != null ? xCurve.getValueAtTime(time) : xDefault;
+        value.y = yCurve != null ? yCurve.getValueAtTime(time) : yDefault;
+        value.z = zCurve != null ? zCurve.getValueAtTime(time) : zDefault;
+        return value;
+    }
+    
+    /**
+     * Converts the euler angles from {@link #getVector3Value(long)} to
+     * a quaternion rotation.
+     * @param time Time at which to get the euler angles.
+     * @return The rotation at time
+     */
+    public Quaternion getQuaternionValue(long time) {
+        Vector3f eulerAngles = getVector3Value(time);
+        System.out.println("\tT: " + time + ". Rotation: " + 
+                                eulerAngles.x + ", " + 
+                                eulerAngles.y + ", " + eulerAngles.z);
+        Quaternion q = new Quaternion();
+        q.fromAngles(eulerAngles.x * FastMath.DEG_TO_RAD, 
+                     eulerAngles.y * FastMath.DEG_TO_RAD, 
+                     eulerAngles.z * FastMath.DEG_TO_RAD);
+        return q;
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        if (!(object instanceof FbxAnimCurve)) {
+            unsupportedConnectObjectProperty(object, property);
+        }
+        if (!property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_X) &&
+            !property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_Y) &&
+            !property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_Z) &&
+            !property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_VISIBILITY)) {
+            logger.log(Level.WARNING, "Animating the dimension ''{0}'' is not "
+                                    + "supported yet. Ignoring.", property);
+            return;
+        }
+        
+        if (propertyToCurveMap.containsKey(property)) {
+            throw new UnsupportedOperationException("!");
+        }
+        
+        propertyToCurveMap.put(property, (FbxAnimCurve) object);
+    }
+    
+}

+ 82 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimLayer.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+public class FbxAnimLayer extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxAnimLayer.class.getName());
+    
+    private final List<FbxAnimCurveNode> animCurves = new ArrayList<FbxAnimCurveNode>();
+    
+    public FbxAnimLayer(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        // No known properties for layers.. 
+        // Also jME3 doesn't support multiple layers anyway.
+    }
+    
+    public List<FbxAnimCurveNode> getAnimationCurveNodes() {
+        return Collections.unmodifiableList(animCurves);
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        if (!(object instanceof FbxAnimCurveNode)) {
+            unsupportedConnectObject(object);
+        }
+        
+        animCurves.add((FbxAnimCurveNode) object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+}
+    

+ 111 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimStack.java

@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxAnimStack extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxAnimStack.class.getName());
+    
+    private float duration;
+    private FbxAnimLayer layer0;
+    
+    public FbxAnimStack(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement child : element.getFbxProperties()) {
+            String propName = (String) child.properties.get(0);
+            if (propName.equals("LocalStop")) {
+                long durationLong = (Long)child.properties.get(4);
+                duration = (float) (durationLong * FbxAnimUtil.SECONDS_PER_UNIT);
+            }
+        }
+    }
+    
+//    /**
+//     * Finds out which FBX nodes this animation is going to influence.
+//     * 
+//     * @return A list of FBX nodes that the stack's curves are influencing.
+//     */
+//    public Set<FbxNode> getInfluencedNodes() {
+//        HashSet<FbxNode> influencedNodes = new HashSet<FbxNode>();
+//        if (layer0 == null) {
+//            return influencedNodes;
+//        }
+//        for (FbxAnimCurveNode curveNode : layer0.getAnimationCurveNodes()) {
+//            influencedNodes.addAll(curveNode.getInfluencedNodes());
+//        }
+//        return influencedNodes;
+//    }
+
+    public float getDuration() {
+        return duration;
+    }
+
+    public FbxAnimLayer[] getLayers() {
+        return new FbxAnimLayer[]{ layer0 };
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+    
+    @Override
+    public void connectObject(FbxObject object) {
+        if (!(object instanceof FbxAnimLayer)) {
+            unsupportedConnectObject(object);
+        }
+        
+        if (layer0 != null) {
+            logger.log(Level.WARNING, "jME3 does not support layered animation. "
+                                    + "Only first layer has been loaded.");
+            return;
+        }
+        
+        layer0 = (FbxAnimLayer) object;
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+}

+ 44 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimUtil.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+public class FbxAnimUtil {
+    /**
+     * Conversion factor from FBX animation time unit to seconds.
+     */
+    public static final double SECONDS_PER_UNIT = 1 / 46186158000d;
+    
+    public static final String CURVE_NODE_PROPERTY_X          = "d|X";
+    public static final String CURVE_NODE_PROPERTY_Y          = "d|Y";
+    public static final String CURVE_NODE_PROPERTY_Z          = "d|Z";
+    public static final String CURVE_NODE_PROPERTY_VISIBILITY = "d|Visibility";
+}

+ 103 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxBindPose.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.math.Matrix4f;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.file.FbxId;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.HashMap;
+import java.util.Map;
+
+public class FbxBindPose extends FbxObject<Map<FbxId, Matrix4f>> {
+
+    private final Map<FbxId, Matrix4f> bindPose = new HashMap<FbxId, Matrix4f>();
+    
+    public FbxBindPose(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement child : element.children) {
+            if (!child.id.equals("PoseNode")) {
+                continue;
+            }
+            
+            FbxId node = null;
+            float[] matData = null;
+            
+            for (FbxElement e : child.children) {
+                if (e.id.equals("Node")) {
+                    node = FbxId.create(e.properties.get(0));
+                } else if (e.id.equals("Matrix")) {
+                    double[] matDataDoubles = (double[]) e.properties.get(0);
+                    
+                    if (matDataDoubles.length != 16) {
+                        // corrupt
+                        throw new UnsupportedOperationException("Bind pose matrix "
+                                + "must have 16 doubles, but it has " 
+                                + matDataDoubles.length + ". Data is corrupt");
+                    }
+                    
+                    matData = new float[16];
+                    for (int i = 0; i < matDataDoubles.length; i++) {
+                        matData[i] = (float) matDataDoubles[i];
+                    }
+                }
+            }
+            
+            if (node != null && matData != null) {
+                Matrix4f matrix = new Matrix4f(matData);
+                bindPose.put(node, matrix);
+            }
+        }
+    }
+    
+    @Override
+    protected Map<FbxId, Matrix4f> toJmeObject() {
+        return bindPose;
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+}

+ 98 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxCluster.java

@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxCluster extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxCluster.class.getName());
+    
+    private int[] indexes;
+    private double[] weights;
+    private FbxLimbNode limb;
+    
+    public FbxCluster(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement e : element.children) {
+            if (e.id.equals("Indexes")) {
+                indexes = (int[]) e.properties.get(0);
+            } else if (e.id.equals("Weights")) {
+                weights = (double[]) e.properties.get(0);
+            }
+        }
+    }
+
+    public int[] getVertexIndices() {
+        return indexes;
+    }
+
+    public double[] getWeights() {
+        return weights;
+    }
+
+    public FbxLimbNode getLimb() {
+        return limb;
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        if (object instanceof FbxLimbNode) {
+            if (limb != null) {
+                logger.log(Level.WARNING, "This cluster already has a limb attached. Ignoring.");
+                return;
+            }
+            limb = (FbxLimbNode) object;
+        } else {
+            unsupportedConnectObject(object);
+        }
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+}

+ 94 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxLimbNode.java

@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FbxLimbNode extends FbxNode {
+    
+    protected FbxNode skeletonHolder;
+    protected Bone bone;
+    
+    public FbxLimbNode(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    private static void createBones(FbxNode skeletonHolderNode, FbxLimbNode limb, List<Bone> bones) {
+        limb.skeletonHolder = skeletonHolderNode;
+        
+        Bone parentBone = limb.getJmeBone();
+        bones.add(parentBone);
+        
+        for (FbxNode child : limb.children) {
+            if (child instanceof FbxLimbNode) {
+                FbxLimbNode childLimb = (FbxLimbNode) child;
+                createBones(skeletonHolderNode, childLimb, bones);
+                parentBone.addChild(childLimb.getJmeBone());
+            }
+        }
+    }
+    
+    public static Skeleton createSkeleton(FbxNode skeletonHolderNode) {
+        if (skeletonHolderNode instanceof FbxLimbNode) {
+            throw new UnsupportedOperationException("Limb nodes cannot be skeleton holders");
+        }
+        
+        List<Bone> bones = new ArrayList<Bone>();
+        
+        for (FbxNode child : skeletonHolderNode.getChildren()) {
+            if (child instanceof FbxLimbNode) {
+                createBones(skeletonHolderNode, (FbxLimbNode) child, bones);
+            }
+        }
+        
+        return new Skeleton(bones.toArray(new Bone[0]));
+    }
+    
+    public FbxNode getSkeletonHolder() {
+        return skeletonHolder;
+    }
+    
+    public Bone getJmeBone() {
+        if (bone == null) {
+            bone = new Bone(name);
+            bone.setBindTransforms(jmeLocalBindPose.getTranslation(),
+                                   jmeLocalBindPose.getRotation(),
+                                   jmeLocalBindPose.getScale());
+        }
+        return bone;
+    }
+}

+ 66 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxSkinDeformer.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FbxSkinDeformer extends FbxObject<List<FbxCluster>> {
+
+    private final List<FbxCluster> clusters = new ArrayList<FbxCluster>();
+    
+    public FbxSkinDeformer(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    protected List<FbxCluster> toJmeObject() {
+        return clusters;
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        if (object instanceof FbxCluster) {
+            clusters.add((FbxCluster) object);
+        } else {
+            unsupportedConnectObject(object);
+        }
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+}

+ 202 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxToJmeTrack.java

@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.anim;
+
+import com.jme3.animation.BoneTrack;
+import com.jme3.animation.SpatialTrack;
+import com.jme3.animation.Track;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Maps animation stacks to influenced nodes. 
+ * Will be used later to create jME3 tracks.
+ */
+public final class FbxToJmeTrack {
+
+    public FbxAnimStack animStack;
+    public FbxAnimLayer animLayer;
+    public FbxNode node;
+
+    // These are not used in map lookups.
+    public transient final Map<String, FbxAnimCurveNode> animCurves = new HashMap<String, FbxAnimCurveNode>();
+
+    public long[] getKeyTimes() {
+        Set<Long> keyFrameTimesSet = new HashSet<Long>();
+        for (FbxAnimCurveNode curveNode : animCurves.values()) {
+            for (FbxAnimCurve curve : curveNode.getCurves()) {
+                for (long keyTime : curve.getKeyTimes()) {
+                    keyFrameTimesSet.add(keyTime);
+                }
+            }
+        }
+        long[] keyFrameTimes = new long[keyFrameTimesSet.size()];
+        int i = 0;
+        for (Long keyFrameTime : keyFrameTimesSet) {
+            keyFrameTimes[i++] = keyFrameTime;
+        }
+        Arrays.sort(keyFrameTimes);
+        return keyFrameTimes;
+    }
+    
+    /**
+     * Generate a {@link BoneTrack} from the animation data, for the given
+     * boneIndex.
+     * 
+     * @param boneIndex The bone index for which track data is generated for.
+     * @param inverseBindPose Inverse bind pose of the bone (in world space).
+     * @return A BoneTrack containing the animation data, for the specified
+     * boneIndex.
+     */
+    public BoneTrack toJmeBoneTrack(int boneIndex, Transform inverseBindPose) {
+        return (BoneTrack) toJmeTrackInternal(boneIndex, inverseBindPose);
+    }
+    
+    public SpatialTrack toJmeSpatialTrack() {
+        return (SpatialTrack) toJmeTrackInternal(-1, null);
+    }
+    
+    public float getDuration() {
+        long[] keyframes = getKeyTimes();
+        return (float) (keyframes[keyframes.length - 1] * FbxAnimUtil.SECONDS_PER_UNIT);
+    }
+    
+    private static void applyInverse(Vector3f translation, Quaternion rotation, Vector3f scale, Transform inverseBindPose) {
+        Transform t = new Transform();
+        t.setTranslation(translation);
+        t.setRotation(rotation);
+        if (scale != null) {
+            t.setScale(scale);
+        }
+        t.combineWithParent(inverseBindPose);
+        
+        t.getTranslation(translation);
+        t.getRotation(rotation);
+        if (scale != null) {
+            t.getScale(scale);
+        }
+    }
+    
+    private Track toJmeTrackInternal(int boneIndex, Transform inverseBindPose) {
+        float duration = animStack.getDuration();
+        
+        FbxAnimCurveNode translationCurve = animCurves.get("Lcl Translation");
+        FbxAnimCurveNode rotationCurve    = animCurves.get("Lcl Rotation");
+        FbxAnimCurveNode scalingCurve     = animCurves.get("Lcl Scaling");
+
+        long[] fbxTimes = getKeyTimes();
+        float[] times = new float[fbxTimes.length];
+        
+        // Translations / Rotations must be set on all tracks.
+        // (Required for jME3)
+        Vector3f[]   translations = new Vector3f[fbxTimes.length];
+        Quaternion[] rotations = new Quaternion[fbxTimes.length];
+        
+        Vector3f[] scales = null;
+        if (scalingCurve != null) {
+            scales = new Vector3f[fbxTimes.length];
+        }
+         
+         for (int i = 0; i < fbxTimes.length; i++) {
+            long fbxTime = fbxTimes[i];
+            float time = (float) (fbxTime * FbxAnimUtil.SECONDS_PER_UNIT);
+
+            if (time > duration) {
+                // Expand animation duration to fit the curve.
+                duration = time;
+                System.out.println("actual duration: " + duration);
+            }
+
+            times[i] = time;
+            if (translationCurve != null) {
+                translations[i] = translationCurve.getVector3Value(fbxTime);
+            } else {
+                translations[i] = new Vector3f();
+            }
+            if (rotationCurve != null) {
+                rotations[i] = rotationCurve.getQuaternionValue(fbxTime);
+                if (i > 0) {
+                    if (rotations[i - 1].dot(rotations[i]) < 0) {
+                        System.out.println("rotation will go the long way, oh noes");
+                        rotations[i - 1].negate();
+                    }
+                }
+            } else {
+                rotations[i] = new Quaternion();
+            }
+            if (scalingCurve != null) {
+                scales[i] = scalingCurve.getVector3Value(fbxTime);
+            }
+            
+            if (inverseBindPose != null) {
+                applyInverse(translations[i], rotations[i], scales != null ? scales[i] : null, inverseBindPose);
+            }
+        }
+        
+        if (boneIndex == -1) {
+            return new SpatialTrack(times, translations, rotations, scales);
+        } else {
+            if (scales != null) {
+                return new BoneTrack(boneIndex, times, translations, rotations, scales);
+            } else {
+                return new BoneTrack(boneIndex, times, translations, rotations);
+            }
+        }
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 79 * hash + this.animStack.hashCode();
+        hash = 79 * hash + this.animLayer.hashCode();
+        hash = 79 * hash + this.node.hashCode();
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        final FbxToJmeTrack other = (FbxToJmeTrack) obj;
+        return this.node == other.node
+                && this.animStack == other.animStack
+                && this.animLayer == other.animLayer;
+    }
+}

+ 190 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxImage.java

@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.material;
+
+import com.jme3.asset.AssetLoadException;
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.AssetNotFoundException;
+import com.jme3.asset.TextureKey;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import com.jme3.texture.Image;
+import com.jme3.util.PlaceholderAssets;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public final class FbxImage extends FbxObject {
+    
+    private static final Logger logger = Logger.getLogger(FbxImage.class.getName());
+    
+    protected TextureKey key;
+    protected String type;             // = "Clip"
+    protected String filePath;         // = "C:\Whatever\Blah\Texture.png"
+    protected String relativeFilePath; // = "..\Blah\Texture.png"
+    protected byte[] content;          // = null, byte[0] OR embedded image data (unknown format?)
+    
+    public FbxImage(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        if (element.propertiesTypes.length == 3) {
+            type = (String) element.properties.get(2);
+        } else {
+            type = (String) element.properties.get(1);
+        }
+        if (type.equals("Clip")) {
+            for (FbxElement e : element.children) {
+                if (e.id.equals("Type")) {
+                    type = (String) e.properties.get(0);
+                } else if (e.id.equals("FileName")) {
+                    filePath = (String) e.properties.get(0);
+                } else if (e.id.equals("RelativeFilename")) {
+                    relativeFilePath = (String) e.properties.get(0);
+                } else if (e.id.equals("Content")) {
+                    if (e.properties.size() > 0) {
+                        byte[] storedContent = (byte[]) e.properties.get(0);
+                        if (storedContent.length > 0) {
+                            this.content = storedContent;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    private Image loadImageSafe(AssetManager assetManager, TextureKey texKey) {
+        try {
+            return assetManager.loadTexture(texKey).getImage();
+        } catch (AssetNotFoundException ex) {
+            return null;
+        } catch (AssetLoadException ex) {
+            logger.log(Level.WARNING, "Error when loading image: " + texKey, ex);
+            return null;
+        }
+    }
+    
+    private static String getFileName(String filePath) {
+        // NOTE: Gotta do it this way because new File().getParent() 
+        // will not strip forward slashes on Linux / Mac OS X.
+        int fwdSlashIdx = filePath.lastIndexOf("\\");
+        int bkSlashIdx = filePath.lastIndexOf("/");
+
+        if (fwdSlashIdx != -1) {
+            filePath = filePath.substring(fwdSlashIdx + 1);
+        } else if (bkSlashIdx != -1) {
+            filePath = filePath.substring(bkSlashIdx + 1);
+        }
+
+        return filePath;
+    }
+    
+    /**
+     * The texture key that was used to load the image.
+     * Only valid after {@link #getJmeObject()} has been called.
+     * @return the key that was used to load the image.
+     */
+    public TextureKey getTextureKey() {
+        return key;
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        Image image = null;
+        String fileName = null;
+        String relativeFilePathJme;
+
+        if (filePath != null) {
+            fileName = getFileName(filePath);
+        } else if (relativeFilePath != null) {
+            fileName = getFileName(relativeFilePath);
+            
+        }
+
+        if (fileName != null) {
+            try {
+                // Try to load filename relative to FBX folder
+                key = new TextureKey(sceneFolderName + fileName);
+                key.setGenerateMips(true);
+                image = loadImageSafe(assetManager, key);
+                
+                // Try to load relative filepath relative to FBX folder
+                if (image == null && relativeFilePath != null) {
+                    // Convert Windows paths to jME3 paths
+                    relativeFilePathJme = relativeFilePath.replace('\\', '/');
+                    key = new TextureKey(sceneFolderName + relativeFilePathJme);
+                    key.setGenerateMips(true);
+                    image = loadImageSafe(assetManager, key);
+                }
+                
+                // Try to load embedded image
+                if (image == null && content != null && content.length > 0) {
+                    key = new TextureKey(fileName);
+                    key.setGenerateMips(true);
+                    InputStream is = new ByteArrayInputStream(content);
+                    image = assetManager.loadAssetFromStream(key, is).getImage();
+                    
+                    // NOTE: embedded texture doesn't exist in the asset manager,
+                    //       so the texture key must not be saved.
+                    key = null;
+                }
+            } catch (AssetLoadException ex) {
+                logger.log(Level.WARNING, "Error while attempting to load texture {0}:\n{1}",
+                           new Object[]{name, ex.toString()});
+            }
+        }
+
+        if (image == null) {
+            logger.log(Level.WARNING, "Cannot locate {0} for texture {1}", new Object[]{fileName, name});
+            image = PlaceholderAssets.getPlaceholderImage(assetManager);
+        }
+        
+        // NOTE: At this point, key will be set to the last
+        //       attempted texture key that we attempted to load.
+
+        return image;
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+}

+ 363 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxMaterial.java

@@ -0,0 +1,363 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.material;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState.BlendMode;
+import com.jme3.material.RenderState.FaceCullMode;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import com.jme3.texture.Texture;
+import com.jme3.texture.image.ColorSpace;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxMaterial extends FbxObject<Material> {
+    
+    private static final Logger logger = Logger.getLogger(FbxMaterial.class.getName());
+    
+    private String shadingModel; // TODO: do we care about this? lambert just has no specular?
+    private final FbxMaterialProperties properties = new FbxMaterialProperties();
+    
+    public FbxMaterial(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        if(!getSubclassName().equals("")) {
+            return;
+        }
+        
+        FbxElement shadingModelEl = element.getChildById("ShadingModel");
+        if (shadingModelEl != null) { 
+            shadingModel = (String) shadingModelEl.properties.get(0);
+            if (!shadingModel.equals("")) {
+                if (!shadingModel.equalsIgnoreCase("phong") &&
+                    !shadingModel.equalsIgnoreCase("lambert")) {
+                    logger.log(Level.WARNING, "FBX material uses unknown shading model: {0}. "
+                                            + "Material may display incorrectly.", shadingModel);  
+                }
+            }
+        }
+        
+        for (FbxElement child : element.getFbxProperties()) {
+            properties.setPropertyFromElement(child);
+        }
+    }
+    
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        if (!(object instanceof FbxTexture)) {
+            unsupportedConnectObjectProperty(object, property);
+        }
+        
+        properties.setPropertyTexture(property, (FbxTexture) object);
+    }
+    
+    private static void multRGB(ColorRGBA color, float factor) {
+        color.r *= factor;
+        color.g *= factor;
+        color.b *= factor;
+    }
+    
+    @Override
+    protected Material toJmeObject() {
+        ColorRGBA ambient  = null;
+        ColorRGBA diffuse  = null;
+        ColorRGBA specular = null;
+        ColorRGBA transp   = null;
+        ColorRGBA emissive = null;
+        float shininess    = 1f;
+        boolean separateTexCoord = false;
+        
+        Texture diffuseMap  = null;
+        Texture specularMap = null;
+        Texture normalMap   = null;
+        Texture transpMap   = null;
+        Texture emitMap     = null;
+        Texture aoMap       = null;
+        
+        FbxTexture fbxDiffuseMap = null;
+        
+        Object diffuseColor = properties.getProperty("DiffuseColor");
+        if (diffuseColor != null) {
+            if (diffuseColor instanceof ColorRGBA) {
+                diffuse = ((ColorRGBA) diffuseColor).clone();
+            } else if (diffuseColor instanceof FbxTexture) {
+                FbxTexture tex = (FbxTexture) diffuseColor;
+                fbxDiffuseMap = tex;
+                diffuseMap = tex.getJmeObject();
+                diffuseMap.getImage().setColorSpace(ColorSpace.sRGB);
+            }
+        }
+        
+        Object diffuseFactor = properties.getProperty("DiffuseFactor");
+        if (diffuseFactor != null && diffuseFactor instanceof Float) {
+            float factor = (Float)diffuseFactor;
+            if (diffuse != null) {
+                multRGB(diffuse, factor);
+            } else {
+                diffuse = new ColorRGBA(factor, factor, factor, 1f);
+            }
+        }
+        
+        Object specularColor = properties.getProperty("SpecularColor");
+        if (specularColor != null) {
+            if (specularColor instanceof ColorRGBA) {
+                specular = ((ColorRGBA) specularColor).clone();
+            } else if (specularColor instanceof FbxTexture) {
+                FbxTexture tex = (FbxTexture) specularColor;
+                specularMap = tex.getJmeObject();
+                specularMap.getImage().setColorSpace(ColorSpace.sRGB);
+            }
+        }
+        
+        Object specularFactor = properties.getProperty("SpecularFactor");
+        if (specularFactor != null && specularFactor instanceof Float) {
+            float factor = (Float)specularFactor;
+            if (specular != null) {
+                multRGB(specular, factor);
+            } else {
+                specular = new ColorRGBA(factor, factor, factor, 1f);
+            }
+        }
+        
+        Object transparentColor = properties.getProperty("TransparentColor");
+        if (transparentColor != null) {
+            if (transparentColor instanceof ColorRGBA) {
+                transp = ((ColorRGBA) transparentColor).clone();
+            } else if (transparentColor instanceof FbxTexture) {
+                FbxTexture tex = (FbxTexture) transparentColor;
+                transpMap = tex.getJmeObject();
+                transpMap.getImage().setColorSpace(ColorSpace.sRGB);
+            }
+        }
+        
+        Object transparencyFactor = properties.getProperty("TransparencyFactor");
+        if (transparencyFactor != null && transparencyFactor instanceof Float) {
+            float factor = (Float)transparencyFactor;
+            if (transp != null) {
+                transp.a *= factor;
+            } else {
+                transp = new ColorRGBA(1f, 1f, 1f, factor);
+            }
+        }
+        
+        Object emissiveColor = properties.getProperty("EmissiveColor");
+        if (emissiveColor != null) {
+            if (emissiveColor instanceof ColorRGBA) {
+                emissive = ((ColorRGBA)emissiveColor).clone();
+            } else if (emissiveColor instanceof FbxTexture) {
+                FbxTexture tex = (FbxTexture) emissiveColor;
+                emitMap = tex.getJmeObject();
+                emitMap.getImage().setColorSpace(ColorSpace.sRGB);
+            }
+        }
+        
+        Object emissiveFactor = properties.getProperty("EmissiveFactor");
+        if (emissiveFactor != null && emissiveFactor instanceof Float) {
+            float factor = (Float)emissiveFactor;
+            if (emissive != null) { 
+                multRGB(emissive, factor);
+            } else {
+                emissive = new ColorRGBA(factor, factor, factor, 1f);
+            }
+        }
+        
+        Object ambientColor = properties.getProperty("AmbientColor");
+        if (ambientColor != null && ambientColor instanceof ColorRGBA) {
+            ambient = ((ColorRGBA)ambientColor).clone();
+        }
+        
+        Object ambientFactor = properties.getProperty("AmbientFactor");
+        if (ambientFactor != null && ambientFactor instanceof Float) { 
+            float factor = (Float)ambientFactor;
+            if (ambient != null) {
+                multRGB(ambient, factor);
+            } else {
+                ambient = new ColorRGBA(factor, factor, factor, 1f);
+            }
+        }
+        
+        Object shininessFactor = properties.getProperty("Shininess");
+        if (shininessFactor != null) {
+            if (shininessFactor instanceof Float) {
+                shininess = (Float) shininessFactor;
+            } else if (shininessFactor instanceof FbxTexture) {
+                // TODO: support shininess textures
+            }
+        }
+        
+        Object bumpNormal = properties.getProperty("NormalMap");
+        if (bumpNormal != null) {
+            if (bumpNormal instanceof FbxTexture) {
+                // TODO: check all meshes that use this material have tangents
+                //       otherwise shading errors occur
+                FbxTexture tex = (FbxTexture) bumpNormal;
+                normalMap = tex.getJmeObject();
+                normalMap.getImage().setColorSpace(ColorSpace.Linear);
+            }
+        }
+        
+        Object aoColor = properties.getProperty("DiffuseColor2");
+        if (aoColor != null) {
+            if (aoColor instanceof FbxTexture) {
+                FbxTexture tex = (FbxTexture) aoColor;
+                if (tex.getUvSet() != null && fbxDiffuseMap != null) {
+                    if (!tex.getUvSet().equals(fbxDiffuseMap.getUvSet())) {
+                        separateTexCoord = true;
+                    }
+                }
+                aoMap = tex.getJmeObject();
+                aoMap.getImage().setColorSpace(ColorSpace.sRGB);
+            }
+        }
+        
+        // TODO: how to disable transparency from diffuse map?? Need "UseAlpha" again..
+
+        assert ambient == null  || ambient.a  == 1f;
+        assert diffuse == null  || diffuse.a  == 1f;
+        assert specular == null || specular.a == 1f;
+        assert emissive == null || emissive.a == 1f;
+        assert transp == null   || (transp.r == 1f && transp.g == 1f && transp.b == 1f);
+        
+        // If shininess is less than 1.0, the lighting shader won't be able
+        // to handle it. Gotta disable specularity then.
+        if (shininess < 1f) {
+            shininess = 1f;
+            specular = ColorRGBA.Black;
+        }
+        
+        // Try to guess if we need to enable alpha blending.
+        // FBX does not specify this explicitly.
+        boolean useAlphaBlend = false;
+        
+        if (diffuseMap != null && diffuseMap == transpMap) {
+            // jME3 already uses alpha from diffuseMap
+            // (if alpha blend is enabled)
+            useAlphaBlend = true;
+            transpMap = null; 
+        } else if (diffuseMap != null && transpMap != null && diffuseMap != transpMap) {
+            // TODO: potential bug here. Alpha from diffuse may 
+            // leak unintentionally. 
+            useAlphaBlend = true;
+        } else if (transpMap != null) {
+            // We have alpha map but no diffuse map, OK.
+            useAlphaBlend = true;
+        }
+        
+        if (transp != null && transp.a != 1f) {
+            // Consolidate transp into diffuse
+            // (jME3 doesn't use a separate alpha color)
+            
+            // TODO: potential bug here. Alpha from diffuse may 
+            // leak unintentionally. 
+            useAlphaBlend = true;
+            if (diffuse != null) {
+                diffuse.a = transp.a;
+            } else {
+                diffuse = transp;
+            }
+        }
+        
+        Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        mat.setName(name);
+        
+        // TODO: load this from FBX material.
+        mat.setReceivesShadows(true);
+        
+        if (useAlphaBlend) {
+            // No idea if this is a transparent or translucent model, gotta guess..
+            mat.setTransparent(true);
+            mat.setFloat("AlphaDiscardThreshold", 0.01f);
+            mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
+        }
+        
+        mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
+        
+        // Set colors.
+        if (ambient != null || diffuse != null || specular != null) {
+            // If either of those is set, we have to set them all.
+            // NOTE: default specular is black, unless it is set explicitly.
+            mat.setBoolean("UseMaterialColors", true);
+            mat.setColor("Ambient",  /*ambient  != null ? ambient  :*/ ColorRGBA.White);
+            mat.setColor("Diffuse",  diffuse  != null ? diffuse  : ColorRGBA.White);
+            mat.setColor("Specular", specular != null ? specular : ColorRGBA.Black);
+        }
+        
+        if (emissive != null) { 
+            mat.setColor("GlowColor", emissive);
+        }
+        
+        // Set shininess.
+        if (shininess > 1f) {
+            // Convert shininess from 
+            // Phong (FBX shading model) to Blinn (jME3 shading model).
+            float blinnShininess = (shininess * 5.1f) + 1f;
+            mat.setFloat("Shininess", blinnShininess);
+        }
+        
+        // Set textures.
+        if (diffuseMap != null) {
+            mat.setTexture("DiffuseMap", diffuseMap);
+        }
+        if (specularMap != null) {
+            mat.setTexture("SpecularMap", specularMap);
+        }
+        if (normalMap != null) {
+            mat.setTexture("NormalMap", normalMap);
+        }
+        if (transpMap != null) {
+//            mat.setTexture("AlphaMap", transpMap);
+        }
+        if (emitMap != null) {
+            mat.setTexture("GlowMap", emitMap);
+        }
+        if (aoMap != null) {
+            mat.setTexture("LightMap", aoMap);
+            if (separateTexCoord) {
+                mat.setBoolean("SeparateTexCoord", true);
+            }
+        }
+        
+        return mat;
+    }
+}

+ 234 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxMaterialProperties.java

@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.material;
+
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxMaterialProperties {
+
+    private static final Logger logger = Logger.getLogger(FbxMaterialProperties.class.getName());
+    
+    private static final Map<String, FBXMaterialProperty> propertyMetaMap = new HashMap<String, FBXMaterialProperty>();
+    
+    private final Map<String, Object> propertyValueMap = new HashMap<String, Object>();
+    
+    private static enum Type {
+        Color,
+        Alpha,
+        Factor,
+        Texture2DOrColor,
+        Texture2DOrAlpha,
+        Texture2DOrFactor,
+        Texture2D,
+        TextureCubeMap,
+        Ignore;
+    }
+    
+    private static class FBXMaterialProperty {
+        private final String name;
+        private final Type type;
+
+        public FBXMaterialProperty(String name, Type type) {
+            this.name = name;
+            this.type = type;
+        }
+    }
+
+    private static boolean isValueAcceptable(Type type, Object value) {
+        if (type == Type.Ignore) {
+            return true;
+        }
+        if (value instanceof FbxTexture) {
+            switch (type) {
+                case Texture2D:
+                case Texture2DOrAlpha:
+                case Texture2DOrColor:
+                case Texture2DOrFactor:
+                    return true;
+            }
+        } else if (value instanceof ColorRGBA) {
+            switch (type) {
+                case Color:
+                case Texture2DOrColor:
+                    return true;
+            }
+        } else if (value instanceof Float) {
+            switch (type) {
+                case Alpha:
+                case Factor:
+                case Texture2DOrAlpha:
+                case Texture2DOrFactor:
+                    return true;
+            }
+        }
+        
+        return false;
+    }
+    
+    private static void defineProp(String name, Type type) {
+        propertyMetaMap.put(name, new FBXMaterialProperty(name, type));
+    }
+    
+    private static void defineAlias(String alias, String name) {
+        propertyMetaMap.put(alias, propertyMetaMap.get(name));
+    }
+    
+    static {
+        // Lighting->Ambient
+        // TODO: Add support for AmbientMap??
+        defineProp("AmbientColor", Type.Color);
+        defineProp("AmbientFactor", Type.Factor);
+        defineAlias("Ambient", "AmbientColor");
+        
+        // Lighting->DiffuseMap/Diffuse
+        defineProp("DiffuseColor", Type.Texture2DOrColor);
+        defineProp("DiffuseFactor", Type.Factor);
+        defineAlias("Diffuse", "DiffuseColor");
+        
+        // Lighting->SpecularMap/Specular
+        defineProp("SpecularColor", Type.Texture2DOrColor);
+        defineProp("SpecularFactor", Type.Factor);
+        defineAlias("Specular", "SpecularColor");
+        
+        // Lighting->AlphaMap/Diffuse
+        defineProp("TransparentColor", Type.Texture2DOrAlpha);
+        
+        // Lighting->Diffuse
+        defineProp("TransparencyFactor", Type.Alpha);
+        defineAlias("Opacity", "TransparencyFactor");
+        
+        // Lighting->GlowMap/GlowColor
+        defineProp("EmissiveColor", Type.Texture2DOrColor);
+        defineProp("EmissiveFactor", Type.Factor);
+        defineAlias("Emissive", "EmissiveColor");
+        
+        // Lighting->Shininess
+        defineProp("Shininess", Type.Factor);
+        defineAlias("ShininessExponent", "Shininess");
+        
+        // Lighting->NormalMap
+        defineProp("NormalMap", Type.Texture2D);
+        defineAlias("Normal", "NormalMap");
+        
+        // Lighting->EnvMap
+        defineProp("ReflectionColor", Type.Texture2DOrColor);
+        
+        // Lighting->FresnelParams
+        defineProp("Reflectivity", Type.Factor);
+        defineAlias("ReflectionFactor", "Reflectivity");
+        
+        // ShadingModel is no longer specified under Properties element.
+        defineProp("ShadingModel", Type.Ignore);
+        
+        // MultiLayer materials aren't supported anyway..
+        defineProp("MultiLayer", Type.Ignore); 
+        
+        // Not sure what this is.. NormalMap again??
+        defineProp("Bump", Type.Texture2DOrColor);
+        
+        defineProp("BumpFactor", Type.Factor);
+        defineProp("DisplacementColor", Type.Color);
+        defineProp("DisplacementFactor", Type.Factor);
+    }
+    
+    public void setPropertyTexture(String name, FbxTexture texture) {
+        FBXMaterialProperty prop = propertyMetaMap.get(name);
+        
+        if (prop == null) {
+            logger.log(Level.WARNING, "Unknown FBX material property '{0}'", name);
+            return; 
+        }
+
+        if (propertyValueMap.get(name) instanceof FbxTexture) {
+            // Multiple / layered textures .. 
+            // Just write into 2nd slot for now (maybe will use for lightmaps).
+            name = name + "2";
+        }
+        
+        propertyValueMap.put(name, texture);
+    }
+    
+    public void setPropertyFromElement(FbxElement propertyElement) {
+        String name = (String) propertyElement.properties.get(0);
+        FBXMaterialProperty prop = propertyMetaMap.get(name);
+        
+        if (prop == null) {
+            logger.log(Level.WARNING, "Unknown FBX material property ''{0}''", name);
+            return; 
+        }
+        
+        // It is either a color, alpha, or factor. 
+        // Textures can only be set via setPropertyTexture.
+        
+        // If it is an alias, get the real name of the property.
+        String realName = prop.name;
+        
+        switch (prop.type) {
+            case Alpha:
+            case Factor:
+            case Texture2DOrFactor:
+            case Texture2DOrAlpha:
+                double value = (Double) propertyElement.properties.get(4);
+                propertyValueMap.put(realName, (float)value);
+                break;
+            case Color:
+            case Texture2DOrColor:
+                double x = (Double) propertyElement.properties.get(4);
+                double y = (Double) propertyElement.properties.get(5);
+                double z = (Double) propertyElement.properties.get(6);
+                ColorRGBA color = new ColorRGBA((float)x, (float)y, (float)z, 1f);
+                propertyValueMap.put(realName, color);
+                break;
+            default:
+                logger.log(Level.WARNING, "FBX material property ''{0}'' requires a texture.", name);
+                break;
+        }
+    }
+    
+    public Object getProperty(String name) { 
+        return propertyValueMap.get(name);
+    }
+    
+    public static Type getPropertyType(String name) {
+        FBXMaterialProperty prop = propertyMetaMap.get(name);
+        if (prop == null) { 
+            return null;
+        } else {
+            return prop.type;
+        }
+    }
+}

+ 146 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/material/FbxTexture.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.material;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.TextureKey;
+import com.jme3.math.Vector2f;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.MagFilter;
+import com.jme3.texture.Texture.MinFilter;
+import com.jme3.texture.Texture.WrapAxis;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.texture.Texture2D;
+import com.jme3.util.PlaceholderAssets;
+
+public class FbxTexture extends FbxObject<Texture> {
+
+    private static enum AlphaSource {
+        None,
+        FromTextureAlpha,
+        FromTextureIntensity;
+    }
+    
+    private String type;
+    private FbxImage media;
+    
+    // TODO: not currently used.
+    private AlphaSource alphaSource = AlphaSource.FromTextureAlpha;
+    private String uvSet;
+    private int wrapModeU = 0, wrapModeV = 0;
+    private final Vector2f uvTranslation = new Vector2f(0, 0);
+    private final Vector2f uvScaling = new Vector2f(1, 1);
+
+    public FbxTexture(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    public String getUvSet() { 
+        return uvSet;
+    }
+    
+    @Override
+    protected Texture toJmeObject() {
+        Image image = null;
+        TextureKey key = null;
+        if (media != null) {
+            image = (Image) media.getJmeObject();
+            key = media.getTextureKey();
+        }
+        if (image == null) {
+            image = PlaceholderAssets.getPlaceholderImage(assetManager);
+        }
+        Texture2D tex = new Texture2D(image);
+        if (key != null) {
+            tex.setKey(key);
+            tex.setName(key.getName());
+            tex.setAnisotropicFilter(key.getAnisotropy());
+        }
+        tex.setMinFilter(MinFilter.Trilinear);
+        tex.setMagFilter(MagFilter.Bilinear);
+        if (wrapModeU == 0) {
+            tex.setWrap(WrapAxis.S, WrapMode.Repeat);
+        }
+        if (wrapModeV == 0) {
+            tex.setWrap(WrapAxis.T, WrapMode.Repeat);
+        }
+        return tex;
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        if (getSubclassName().equals("")) {
+            for (FbxElement e : element.children) {
+                if (e.id.equals("Type")) {
+                    type = (String) e.properties.get(0);
+                } 
+                /*else if (e.id.equals("FileName")) {
+                    filename = (String) e.properties.get(0);
+                }*/
+            }
+            
+            for (FbxElement prop : element.getFbxProperties()) {
+                String propName = (String) prop.properties.get(0);
+                if (propName.equals("AlphaSource")) {
+                    // ???
+                } else if (propName.equals("UVSet")) {
+                    uvSet = (String) prop.properties.get(4);
+                } else if (propName.equals("WrapModeU")) {
+                    wrapModeU = (Integer) prop.properties.get(4);
+                } else if (propName.equals("WrapModeV")) {
+                    wrapModeV = (Integer) prop.properties.get(4);
+                }
+            }
+        }
+    }
+    
+    @Override
+    public void connectObject(FbxObject object) {
+        if (!(object instanceof FbxImage)) {
+            unsupportedConnectObject(object);
+//        } else if (media != null) {
+//            throw new UnsupportedOperationException("An image is already attached to this texture.");
+        }
+        
+        this.media = (FbxImage) object;
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+}

+ 107 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxLayer.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.mesh;
+
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public final class FbxLayer {
+    
+    private static final Logger logger = Logger.getLogger(FbxLayer.class.getName());
+    
+    public static class FbxLayerElementRef {
+        FbxLayerElement.Type layerElementType;
+        int layerElementIndex;
+        FbxLayerElement layerElement;
+    }
+    
+    int layer;
+    final EnumMap<FbxLayerElement.Type, FbxLayerElementRef> references = 
+            new EnumMap<FbxLayerElement.Type, FbxLayerElementRef>(FbxLayerElement.Type.class);
+    
+    private FbxLayer() { }
+    
+    public Object getVertexData(FbxLayerElement.Type type, int polygonIndex, 
+                                int polygonVertexIndex, int positionIndex, int edgeIndex) {
+        FbxLayerElementRef reference = references.get(type);
+        if (reference == null) { 
+            return null;
+        } else {
+            return reference.layerElement.getVertexData(polygonIndex, polygonVertexIndex, positionIndex, edgeIndex);
+        }
+    }
+    
+    public FbxLayerElement.Type[] getLayerElementTypes() {
+        FbxLayerElement.Type[] types = new FbxLayerElement.Type[references.size()];
+        references.keySet().toArray(types);
+        return types;
+    }
+    
+    public void setLayerElements(Collection<FbxLayerElement> layerElements) {
+        for (FbxLayerElement layerElement : layerElements) {
+            FbxLayerElementRef reference = references.get(layerElement.type);
+            if (reference != null && reference.layerElementIndex == layerElement.index) {
+                reference.layerElement = layerElement;
+            }
+        }
+    }
+    
+    public static FbxLayer fromElement(FbxElement element) {
+        FbxLayer layer = new FbxLayer();
+        layer.layer = (Integer)element.properties.get(0);
+        next_element: for (FbxElement child : element.children) {
+            if (!child.id.equals("LayerElement")) {
+                continue;
+            }
+            FbxLayerElementRef ref = new FbxLayerElementRef();
+            for (FbxElement child2 : child.children) {                
+                if (child2.id.equals("Type")) {
+                    String layerElementTypeStr = (String) child2.properties.get(0);
+                    layerElementTypeStr = layerElementTypeStr.substring("LayerElement".length());
+                    try {
+                        ref.layerElementType = FbxLayerElement.Type.valueOf(layerElementTypeStr);
+                    } catch (IllegalArgumentException ex) {
+                        logger.log(Level.WARNING, "Unsupported layer type: {0}. Ignoring.", layerElementTypeStr);
+                        continue next_element;
+                    }
+                } else if (child2.id.equals("TypedIndex")) {
+                    ref.layerElementIndex = (Integer) child2.properties.get(0);
+                }
+            }
+            layer.references.put(ref.layerElementType, ref);
+        }
+        return layer;
+    }
+}

+ 243 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxLayerElement.java

@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.mesh;
+
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxLayerElement {
+    
+    private static final Logger logger = Logger.getLogger(FbxLayerElement.class.getName());
+    
+    public enum Type {
+        Position,      // Vector3f (isn't actually defined in FBX)
+        BoneIndex,     // List<Integer> (isn't actually defined in FBX)
+        BoneWeight,    // List<Float> isn't actually defined in FBX)
+        Normal,        // Vector3f
+        Binormal,      // Vector3f
+        Tangent,       // Vector3f
+        UV,            // Vector2f
+        TransparentUV, // Vector2f
+        Color,         // ColorRGBA
+        Material,      // Integer
+        Smoothing,     // Integer
+        Visibility,    // Integer
+        Texture,      // ??? (FBX 6.x)
+        PolygonGroup, // ??? (FBX 6.x)
+        NormalMapTextures, // ??? (FBX 6.x)
+        SpecularFactorUV, // ??? (FBX 6.x)
+        NormalMapUV, // ??? (FBX 6.x)
+        SpecularFactorTextures, // ??? (FBX 6.x)
+        
+    }
+    
+    public enum MappingInformationType {
+        NoMappingInformation,
+        AllSame,
+        ByPolygonVertex,
+        ByVertex,
+        ByPolygon,
+        ByEdge;
+    }
+    
+    public enum ReferenceInformationType {
+        Direct,
+        IndexToDirect;
+    }
+    
+    public enum TextureBlendMode {
+        Translucent;
+    }
+    
+    private static final Set<String> indexTypes = new HashSet<String>();
+    
+    static {
+        indexTypes.add("UVIndex");
+        indexTypes.add("NormalsIndex");
+        indexTypes.add("TangentsIndex");
+        indexTypes.add("BinormalsIndex");
+        indexTypes.add("Smoothing");
+        indexTypes.add("Materials");
+        indexTypes.add("TextureId");
+        indexTypes.add("ColorIndex");
+        indexTypes.add("PolygonGroup");
+    }
+    
+    int index;
+    Type type;
+    ReferenceInformationType refInfoType;
+    MappingInformationType mapInfoType;
+    String name = "";
+    Object[] data;
+    int[] dataIndices;
+
+    private FbxLayerElement() { }
+    
+    public String toString() {
+        return "LayerElement[type=" + type + ", layer=" + index +
+               ", mapInfoType=" + mapInfoType + ", refInfoType=" + refInfoType + "]";
+    }
+    
+    private Object getVertexDataIndexToDirect(int polygonIndex, int polygonVertexIndex, 
+                                              int positionIndex, int edgeIndex) {
+        switch (mapInfoType) {
+            case AllSame:           return data[dataIndices[0]];
+            case ByPolygon:         return data[dataIndices[polygonIndex]];
+            case ByPolygonVertex:   return data[dataIndices[polygonVertexIndex]];
+            case ByVertex:          return data[dataIndices[positionIndex]];
+            case ByEdge:            return data[dataIndices[edgeIndex]];
+            default:                throw new UnsupportedOperationException();
+        }
+    }
+    
+    private Object getVertexDataDirect(int polygonIndex, int polygonVertexIndex, 
+                                              int positionIndex, int edgeIndex) {
+        switch (mapInfoType) {
+            case AllSame:           return data[0];
+            case ByPolygon:         return data[polygonIndex];
+            case ByPolygonVertex:   return data[polygonVertexIndex];
+            case ByVertex:          return data[positionIndex];
+            case ByEdge:            return data[edgeIndex];
+            default:                throw new UnsupportedOperationException();
+        }
+    }
+    
+    public Object getVertexData(int polygonIndex, int polygonVertexIndex, int positionIndex, int edgeIndex) {
+        switch (refInfoType) {
+            case Direct:        return getVertexDataDirect(polygonIndex, polygonVertexIndex, positionIndex, edgeIndex);
+            case IndexToDirect: return getVertexDataIndexToDirect(polygonIndex, polygonVertexIndex, positionIndex, edgeIndex);
+            default:            return null;
+        }
+    }
+    
+    public static FbxLayerElement fromPositions(double[] positionData) {
+        FbxLayerElement layerElement = new FbxLayerElement();
+        layerElement.index = -1;
+        layerElement.name = "";
+        layerElement.type = Type.Position;
+        layerElement.mapInfoType = MappingInformationType.ByVertex;
+        layerElement.refInfoType = ReferenceInformationType.Direct;
+        layerElement.data = toVector3(positionData);
+        layerElement.dataIndices = null;
+        return layerElement;
+    }
+    
+    public static FbxLayerElement fromElement(FbxElement element) {
+        FbxLayerElement layerElement = new FbxLayerElement();
+        if (!element.id.startsWith("LayerElement")) {
+            throw new IllegalArgumentException("Not a layer element");
+        }
+        layerElement.index = (Integer)element.properties.get(0);
+        
+        String elementType = element.id.substring("LayerElement".length());
+        try {
+            layerElement.type = Type.valueOf(elementType);
+        } catch (IllegalArgumentException ex) {
+            logger.log(Level.WARNING, "Unsupported layer element: {0}. Ignoring.", elementType);
+        }
+        for (FbxElement child : element.children) {
+            if (child.id.equals("MappingInformationType")) {
+                String mapInfoTypeVal = (String) child.properties.get(0);
+                if (mapInfoTypeVal.equals("ByVertice")) {
+                    mapInfoTypeVal = "ByVertex";
+                }
+                layerElement.mapInfoType = MappingInformationType.valueOf(mapInfoTypeVal);
+            } else if (child.id.equals("ReferenceInformationType")) {
+                String refInfoTypeVal = (String) child.properties.get(0);
+                if (refInfoTypeVal.equals("Index")) {
+                    refInfoTypeVal = "IndexToDirect";
+                }
+                layerElement.refInfoType = ReferenceInformationType.valueOf(refInfoTypeVal);
+            } else if (child.id.equals("Normals") || child.id.equals("Tangents") || child.id.equals("Binormals")) {
+                layerElement.data = toVector3(FbxMeshUtil.getDoubleArray(child));
+            } else if (child.id.equals("Colors")) {
+                layerElement.data = toColorRGBA(FbxMeshUtil.getDoubleArray(child));
+            } else if (child.id.equals("UV")) {
+                layerElement.data = toVector2(FbxMeshUtil.getDoubleArray(child));
+            } else if (indexTypes.contains(child.id)) {
+                layerElement.dataIndices = FbxMeshUtil.getIntArray(child);
+            } else if (child.id.equals("Name")) {
+                layerElement.name = (String) child.properties.get(0);
+            }
+        }
+        if (layerElement.data == null) {
+            // For Smoothing / Materials, data = dataIndices
+            layerElement.refInfoType = ReferenceInformationType.Direct;
+            layerElement.data = new Integer[layerElement.dataIndices.length];
+            for (int i = 0; i < layerElement.data.length; i++) {
+                layerElement.data[i] = layerElement.dataIndices[i];
+            }
+            layerElement.dataIndices = null;
+        }
+        return layerElement;
+    }
+    
+    static Vector3f[] toVector3(double[] data) {
+        Vector3f[] vectors = new Vector3f[data.length / 3];
+        for (int i = 0; i < vectors.length; i++) {
+            float x = (float) data[i * 3];
+            float y = (float) data[i * 3 + 1];
+            float z = (float) data[i * 3 + 2];
+            vectors[i] = new Vector3f(x, y, z);
+        }
+        return vectors;
+    }
+
+    static Vector2f[] toVector2(double[] data) {
+        Vector2f[] vectors = new Vector2f[data.length / 2];
+        for (int i = 0; i < vectors.length; i++) {
+            float x = (float) data[i * 2];
+            float y = (float) data[i * 2 + 1];
+            vectors[i] = new Vector2f(x, y);
+        }
+        return vectors;
+    }
+    
+    static ColorRGBA[] toColorRGBA(double[] data) {
+        ColorRGBA[] colors = new ColorRGBA[data.length / 4];
+        for (int i = 0; i < colors.length; i++) {
+            float r = (float) data[i * 4];
+            float g = (float) data[i * 4 + 1];
+            float b = (float) data[i * 4 + 2];
+            float a = (float) data[i * 4 + 3];
+            colors[i] = new ColorRGBA(r, g, b, a);
+        }
+        return colors;
+    }
+}
+

+ 316 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java

@@ -0,0 +1,316 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.mesh;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.asset.AssetManager;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.plugins.IrUtils;
+import com.jme3.scene.plugins.IrBoneWeightIndex;
+import com.jme3.scene.plugins.IrMesh;
+import com.jme3.scene.plugins.IrPolygon;
+import com.jme3.scene.plugins.IrVertex;
+import com.jme3.scene.plugins.fbx.anim.FbxCluster;
+import com.jme3.scene.plugins.fbx.anim.FbxLimbNode;
+import com.jme3.scene.plugins.fbx.anim.FbxSkinDeformer;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import com.jme3.scene.plugins.fbx.node.FbxNodeAttribute;
+import com.jme3.util.IntMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public final class FbxMesh extends FbxNodeAttribute<IntMap<Mesh>> {
+
+    private static final Logger logger = Logger.getLogger(FbxMesh.class.getName());
+    
+    private FbxPolygon[] polygons;
+    private int[] edges;
+    private FbxLayerElement[] layerElements;
+    private Vector3f[] positions;
+    private FbxLayer[] layers;
+
+    private ArrayList<Integer>[] boneIndices;
+    private ArrayList<Float>[] boneWeights;
+    
+    private FbxSkinDeformer skinDeformer;
+    
+    public FbxMesh(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        
+        List<FbxLayerElement> layerElementsList = new ArrayList<FbxLayerElement>();
+        List<FbxLayer> layersList = new ArrayList<FbxLayer>();
+        
+        for (FbxElement e : element.children) {
+            if (e.id.equals("Vertices")) {
+                setPositions(FbxMeshUtil.getDoubleArray(e));
+            } else if (e.id.equals("PolygonVertexIndex")) {
+                setPolygonVertexIndices(FbxMeshUtil.getIntArray(e));
+            } else if (e.id.equals("Edges")) {
+                setEdges(FbxMeshUtil.getIntArray(e));
+            } else if (e.id.startsWith("LayerElement")) {
+                layerElementsList.add(FbxLayerElement.fromElement(e));
+            } else if (e.id.equals("Layer")) {
+                layersList.add(FbxLayer.fromElement(e));
+            }
+        }
+        
+        for (FbxLayer layer : layersList) {
+            layer.setLayerElements(layerElementsList);
+        }
+        
+        layerElements = new FbxLayerElement[layerElementsList.size()];
+        layerElementsList.toArray(layerElements);
+        
+        layers = new FbxLayer[layersList.size()];
+        layersList.toArray(layers);
+    }
+
+    public FbxSkinDeformer getSkinDeformer() {
+        return skinDeformer;
+    }
+    
+    public void applyCluster(FbxCluster cluster) {
+        if (boneIndices == null) {
+            boneIndices = new ArrayList[positions.length];
+            boneWeights = new ArrayList[positions.length];
+        }
+        
+        FbxLimbNode limb = cluster.getLimb();
+        Bone bone = limb.getJmeBone();
+        Skeleton skeleton = limb.getSkeletonHolder().getJmeSkeleton();
+        int boneIndex = skeleton.getBoneIndex(bone);
+        
+        int[] positionIndices = cluster.getVertexIndices();
+        double[] weights = cluster.getWeights();
+        
+        for (int i = 0; i < positionIndices.length; i++) {
+            int positionIndex = positionIndices[i];
+            float boneWeight = (float)weights[i];
+            
+            ArrayList<Integer> boneIndicesForVertex = boneIndices[positionIndex];
+            ArrayList<Float>  boneWeightsForVertex = boneWeights[positionIndex];
+            
+            if (boneIndicesForVertex == null) {
+                boneIndicesForVertex = new ArrayList<Integer>();
+                boneWeightsForVertex = new ArrayList<Float>();
+                boneIndices[positionIndex] = boneIndicesForVertex;
+                boneWeights[positionIndex] = boneWeightsForVertex;
+            }
+            
+            boneIndicesForVertex.add(boneIndex);
+            boneWeightsForVertex.add(boneWeight);
+        }
+    }
+    
+    @Override
+    public void connectObject(FbxObject object) {
+        if (object instanceof FbxSkinDeformer) {
+            if (skinDeformer != null) {
+                logger.log(Level.WARNING, "This mesh already has a skin deformer attached. Ignoring.");
+                return;
+            }
+            skinDeformer = (FbxSkinDeformer) object;
+        } else {
+            unsupportedConnectObject(object);
+        }
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+    private void setPositions(double[] positions) {
+        this.positions = FbxLayerElement.toVector3(positions);
+    }
+    
+    private void setEdges(int[] edges) {
+        this.edges = edges;
+        // TODO: ...
+    }
+    
+    private void setPolygonVertexIndices(int[] polygonVertexIndices) {
+        List<FbxPolygon> polygonList = new ArrayList<FbxPolygon>();
+
+        boolean finishPolygon = false;
+        List<Integer> vertexIndices = new ArrayList<Integer>();
+
+        for (int i = 0; i < polygonVertexIndices.length; i++) {
+            int vertexIndex = polygonVertexIndices[i];
+            
+            if (vertexIndex < 0) {
+                vertexIndex ^= -1;
+                finishPolygon = true;
+            }
+            
+            vertexIndices.add(vertexIndex);
+
+            if (finishPolygon) {
+                finishPolygon = false;
+                polygonList.add(FbxPolygon.fromIndices(vertexIndices));
+                vertexIndices.clear();
+            }
+        }
+        
+        polygons = new FbxPolygon[polygonList.size()];
+        polygonList.toArray(polygons);
+    }
+    
+    private static IrBoneWeightIndex[] toBoneWeightIndices(List<Integer> boneIndices, List<Float> boneWeights) {
+        IrBoneWeightIndex[] boneWeightIndices = new IrBoneWeightIndex[boneIndices.size()];
+        for (int i = 0; i < boneIndices.size(); i++) {
+            boneWeightIndices[i] = new IrBoneWeightIndex(boneIndices.get(i), boneWeights.get(i));
+        }
+        return boneWeightIndices;
+    }
+    
+    @Override
+    protected IntMap<Mesh> toJmeObject() {
+        // Load clusters from SkinDeformer
+        if (skinDeformer != null) {
+            for (FbxCluster cluster : skinDeformer.getJmeObject()) {
+                applyCluster(cluster);
+            }
+        }
+        
+        IrMesh irMesh = toIRMesh();
+        
+        // Trim bone weights to 4 weights per vertex.
+        IrUtils.trimBoneWeights(irMesh);
+        
+        // Convert tangents / binormals to tangents with parity.
+        IrUtils.toTangentsWithParity(irMesh);
+        
+        // Triangulate quads.
+        IrUtils.triangulate(irMesh);
+        
+        // Split meshes by material indices.
+        IntMap<IrMesh> irMeshes = IrUtils.splitByMaterial(irMesh);
+        
+        // Create a jME3 Mesh for each material index.
+        IntMap<Mesh> jmeMeshes = new IntMap<Mesh>();
+        for (IntMap.Entry<IrMesh> irMeshEntry : irMeshes) {
+            Mesh jmeMesh = IrUtils.convertIrMeshToJmeMesh(irMeshEntry.getValue());
+            jmeMeshes.put(irMeshEntry.getKey(), jmeMesh);
+        }
+       
+        if (jmeMeshes.size() == 0) {
+            // When will this actually happen? Not sure.
+            logger.log(Level.WARNING, "Empty FBX mesh found (unusual).");
+        }
+        
+        // IMPORTANT: If we have a -1 entry, those are triangles
+        // with no material indices. 
+        // It makes sense only if the mesh uses a single material!
+        if (jmeMeshes.containsKey(-1) && jmeMeshes.size() > 1) {
+            logger.log(Level.WARNING, "Mesh has polygons with no material "
+                                    + "indices (unusual) - they will use material index 0.");
+        }
+        
+        return jmeMeshes;
+    }
+    
+    /**
+     * Convert FBXMesh to IRMesh.
+     */
+    public IrMesh toIRMesh() {
+        IrMesh newMesh = new IrMesh();
+        newMesh.polygons = new IrPolygon[polygons.length];
+        
+        int polygonVertexIndex = 0;
+        int positionIndex = 0;
+        
+        FbxLayer layer0 = layers[0];
+        FbxLayer layer1 = layers.length > 1 ? layers[1] : null;
+        
+        for (int i = 0; i < polygons.length; i++) {
+            FbxPolygon polygon = polygons[i];
+            IrPolygon irPolygon = new IrPolygon();
+            irPolygon.vertices = new IrVertex[polygon.indices.length];
+            
+            for (int j = 0; j < polygon.indices.length; j++) {
+                positionIndex = polygon.indices[j];
+                
+                IrVertex irVertex = new IrVertex();
+                irVertex.pos = positions[positionIndex];
+                
+                if (layer0 != null) {
+                    irVertex.norm      = (Vector3f)  layer0.getVertexData(FbxLayerElement.Type.Normal,   i, polygonVertexIndex, positionIndex, 0);
+                    irVertex.tang      = (Vector3f)  layer0.getVertexData(FbxLayerElement.Type.Tangent,  i, polygonVertexIndex, positionIndex, 0);
+                    irVertex.bitang    = (Vector3f)  layer0.getVertexData(FbxLayerElement.Type.Binormal,  i, polygonVertexIndex, positionIndex, 0);
+                    irVertex.uv0       = (Vector2f)  layer0.getVertexData(FbxLayerElement.Type.UV,       i, polygonVertexIndex, positionIndex, 0);
+                    irVertex.color     = (ColorRGBA) layer0.getVertexData(FbxLayerElement.Type.Color,    i, polygonVertexIndex, positionIndex, 0);
+                    irVertex.material  = (Integer)   layer0.getVertexData(FbxLayerElement.Type.Material,  i, polygonVertexIndex, positionIndex, 0);
+                    irVertex.smoothing = (Integer)   layer0.getVertexData(FbxLayerElement.Type.Smoothing, i, polygonVertexIndex, positionIndex, 0);
+                }
+                
+                if (layer1 != null) {
+                    irVertex.uv1 = (Vector2f) layer1.getVertexData(FbxLayerElement.Type.UV, i, 
+                                                                   polygonVertexIndex, positionIndex, 0);
+                }
+                
+                if (boneIndices != null) {
+                    ArrayList<Integer> boneIndicesForVertex = boneIndices[positionIndex];
+                    ArrayList<Float>   boneWeightsForVertex = boneWeights[positionIndex];
+                    if (boneIndicesForVertex != null) {
+                        irVertex.boneWeightsIndices = toBoneWeightIndices(boneIndicesForVertex, boneWeightsForVertex);
+                    }
+                }
+                
+                irPolygon.vertices[j] = irVertex;
+
+                polygonVertexIndex++;
+            }
+            
+            newMesh.polygons[i] = irPolygon;
+        }
+        
+        // Ensure "inspection vertex" specifies that mesh has bone indices / weights
+        if (boneIndices != null && newMesh.polygons[0].vertices[0] == null) {
+            newMesh.polygons[0].vertices[0].boneWeightsIndices = new IrBoneWeightIndex[0];
+        }
+        
+        return newMesh;
+    }
+}

+ 69 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMeshUtil.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.mesh;
+
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+
+public class FbxMeshUtil {
+
+    public static double[] getDoubleArray(FbxElement el) {
+        if (el.propertiesTypes[0] == 'd') {
+            // FBX 7.x
+            return (double[]) el.properties.get(0);
+        } else if (el.propertiesTypes[0] == 'D') {
+            // FBX 6.x
+            double[] doubles = new double[el.propertiesTypes.length];
+            for (int i = 0; i < doubles.length; i++) {
+                doubles[i] = (Double) el.properties.get(i);
+            }
+            return doubles;
+        } else {
+            return null;
+        }
+    }
+    
+    public static int[] getIntArray(FbxElement el) {
+        if (el.propertiesTypes[0] == 'i') {
+            // FBX 7.x
+            return (int[]) el.properties.get(0);
+        } else if (el.propertiesTypes[0] == 'I') {
+            // FBX 6.x
+            int[] ints = new int[el.propertiesTypes.length];
+            for (int i = 0; i < ints.length; i++) {
+                ints[i] = (Integer) el.properties.get(i);
+            }
+            return ints;
+        } else {
+            return null;
+        }
+    }
+}

+ 59 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxPolygon.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.mesh;
+
+import java.util.Arrays;
+import java.util.List;
+
+public final class FbxPolygon {
+
+    int[] indices;
+
+    @Override
+    public String toString() {
+        return Arrays.toString(indices);
+    }
+    
+    private static int[] listToArray(List<Integer> indices) {
+        int[] indicesArray = new int[indices.size()];
+        for (int i = 0; i < indices.size(); i++) {
+            indicesArray[i] = indices.get(i);
+        }
+        return indicesArray;
+    }
+    
+    public static FbxPolygon fromIndices(List<Integer> indices) {
+        FbxPolygon poly = new FbxPolygon();
+        poly.indices = listToArray(indices);
+        return poly;
+    }
+}

+ 145 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/misc/FbxGlobalSettings.java

@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.misc;
+
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxGlobalSettings {
+    
+    private static final Logger logger = Logger.getLogger(FbxGlobalSettings.class.getName());
+    
+    private static final Map<Integer, Float> timeModeToFps = new HashMap<Integer, Float>();
+    
+    static {
+        timeModeToFps.put(1,  120f);
+        timeModeToFps.put(2,  100f);
+        timeModeToFps.put(3,   60f);
+        timeModeToFps.put(4,   50f);
+        timeModeToFps.put(5,   48f);
+        timeModeToFps.put(6,   30f);
+        timeModeToFps.put(9,   30f / 1.001f);
+        timeModeToFps.put(10,  25f);
+        timeModeToFps.put(11,  24f);
+        timeModeToFps.put(13,  24f / 1.001f);
+        timeModeToFps.put(14, -1f);
+        timeModeToFps.put(15,  96f);
+        timeModeToFps.put(16,  72f);
+        timeModeToFps.put(17,  60f / 1.001f);
+    }
+    
+    public float unitScaleFactor  = 1.0f;
+    public ColorRGBA ambientColor = ColorRGBA.Black;
+    public float frameRate  = 25.0f;
+    
+    /**
+     * @return A {@link Transform} that converts from the FBX file coordinate
+     * system to jME3 coordinate system. 
+     * jME3's coordinate system is:
+     * <ul>
+     * <li>Units are specified in meters.</li>
+     * <li>Orientation is right-handed with Y-up.</li>
+     * </ul>
+     */
+    public Transform getGlobalTransform() {
+        // Default unit scale factor is 1 (centimeters),
+        // convert to meters.
+        float scale = unitScaleFactor / 100.0f;
+        
+        // TODO: handle rotation
+        
+        return new Transform(Vector3f.ZERO, Quaternion.IDENTITY, new Vector3f(scale, scale, scale));
+    }
+    
+    public void fromElement(FbxElement element) {
+        // jME3 uses a +Y up, -Z forward coordinate system (same as OpenGL)
+        // Luckily enough, this is also the default for FBX models.
+        
+        int timeMode = -1;
+        float customFrameRate = 30.0f;
+        
+        for (FbxElement e2 : element.getFbxProperties()) {
+            String propName = (String) e2.properties.get(0);
+            if (propName.equals("UnitScaleFactor")) {
+                unitScaleFactor = ((Double) e2.properties.get(4)).floatValue();
+                if (unitScaleFactor != 100.0f) {
+                    logger.log(Level.WARNING, "FBX model isn't using meters for world units. Scale could be incorrect.");
+                }
+            } else if (propName.equals("TimeMode")) {
+                timeMode = (Integer) e2.properties.get(4);
+            } else if (propName.equals("CustomFrameRate")) {
+                float framerate = ((Double) e2.properties.get(4)).floatValue();
+                if (framerate != -1) {
+                    customFrameRate = framerate;
+                }
+            } else if (propName.equals("UpAxis")) {
+                Integer upAxis = (Integer) e2.properties.get(4);
+                if (upAxis != 1) {
+                    logger.log(Level.WARNING, "FBX model isn't using Y as up axis. Orientation could be incorrect");
+                }
+            } else if (propName.equals("UpAxisSign")) {
+                Integer upAxisSign = (Integer) e2.properties.get(4);
+                if (upAxisSign != 1) {
+                    logger.log(Level.WARNING, "FBX model isn't using correct up axis sign. Orientation could be incorrect");
+                }
+            } else if (propName.equals("FrontAxis")) {
+                Integer frontAxis = (Integer) e2.properties.get(4);
+                if (frontAxis != 2) {
+                    logger.log(Level.WARNING, "FBX model isn't using Z as forward axis. Orientation could be incorrect");
+                }
+            } else if (propName.equals("FrontAxisSign")) {
+                Integer frontAxisSign = (Integer) e2.properties.get(4);
+                if (frontAxisSign != -1) {
+                    logger.log(Level.WARNING, "FBX model isn't using correct forward axis sign. Orientation could be incorrect");
+                }
+            }
+        }
+        
+        Float fps = timeModeToFps.get(timeMode);
+        if (fps != null) {
+            if (fps == -1f) {
+                // Using custom framerate
+                frameRate = customFrameRate;
+            } else {
+                // Use FPS from time mode.
+                frameRate = fps;
+            }
+        }
+    }
+}

+ 617 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java

@@ -0,0 +1,617 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.node;
+
+import com.jme3.animation.AnimControl;
+import com.jme3.animation.Skeleton;
+import com.jme3.animation.SkeletonControl;
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue.Bucket;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.Spatial.CullHint;
+import com.jme3.scene.debug.SkeletonDebugger;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimCurveNode;
+import com.jme3.scene.plugins.fbx.anim.FbxCluster;
+import com.jme3.scene.plugins.fbx.anim.FbxLimbNode;
+import com.jme3.scene.plugins.fbx.anim.FbxSkinDeformer;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.material.FbxImage;
+import com.jme3.scene.plugins.fbx.material.FbxMaterial;
+import com.jme3.scene.plugins.fbx.material.FbxTexture;
+import com.jme3.scene.plugins.fbx.mesh.FbxMesh;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import com.jme3.util.IntMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxNode extends FbxObject<Spatial> {
+
+    private static final Logger logger = Logger.getLogger(FbxNode.class.getName());
+    
+    private static enum InheritMode {
+        /**
+         * Apply parent scale after child rotation.
+         * This is the only mode correctly supported by jME3.
+         */
+        ScaleAfterChildRotation,
+        
+        /**
+         * Apply parent scale before child rotation.
+         * Not supported by jME3, will cause distortion with
+         * non-uniform scale. No way around it.
+         */
+        ScaleBeforeChildRotation,
+        
+        /**
+         * Do not apply parent scale at all.
+         * Not supported by jME3, will cause distortion.
+         * Could be worked around by via: 
+         * <code>jmeChildScale = jmeParentScale / fbxChildScale</code>
+         */
+        NoParentScale
+    }
+    
+    private InheritMode inheritMode = InheritMode.ScaleAfterChildRotation;
+
+    protected FbxNode parent;
+    protected List<FbxNode> children = new ArrayList<FbxNode>();
+    protected List<FbxMaterial> materials = new ArrayList<FbxMaterial>();
+    protected Map<String, Object> userData = new HashMap<String, Object>();
+    protected Map<String, List<FbxAnimCurveNode>> propertyToAnimCurveMap = new HashMap<String, List<FbxAnimCurveNode>>();
+    protected FbxNodeAttribute nodeAttribute;
+    protected double visibility = 1.0;
+    
+    /**
+     * For FBX nodes that contain a skeleton (i.e. FBX limbs).
+     */
+    protected Skeleton skeleton;
+    
+    protected final Transform jmeWorldNodeTransform = new Transform();
+    protected final Transform jmeLocalNodeTransform = new Transform();
+
+    // optional - used for limbs / bones / skeletons
+    protected Transform jmeWorldBindPose;
+    protected Transform jmeLocalBindPose;
+    
+    // used for debugging only
+    protected Matrix4f cachedWorldBindPose;
+    
+    public FbxNode(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    public Transform computeFbxLocalTransform() {
+        // TODO: implement the actual algorithm, which is this:
+        // Render Local Translation =
+        //      Inv Scale Pivot * Lcl Scale * Scale Pivot * Scale Offset * Inv Rota Pivot * Post Rotation * Rotation * Pre Rotation * Rotation Pivot * Rotation Offset * Translation
+        
+        // LclTranslation, 
+        // LclRotation, 
+        // PreRotation, 
+        // PostRotation, 
+        // RotationPivot, 
+        // RotationOffset, 
+        // LclScaling, 
+        // ScalingPivot, 
+        // ScalingOffset
+        
+        Matrix4f scaleMat = new Matrix4f();
+        scaleMat.setScale(jmeLocalNodeTransform.getScale());
+        
+        Matrix4f rotationMat = new Matrix4f();
+        rotationMat.setRotationQuaternion(jmeLocalNodeTransform.getRotation());
+        
+        Matrix4f translationMat = new Matrix4f();
+        translationMat.setTranslation(jmeLocalNodeTransform.getTranslation());
+        
+        Matrix4f result = new Matrix4f();
+        result.multLocal(scaleMat).multLocal(rotationMat).multLocal(translationMat);
+        
+        Transform t = new Transform();
+        t.fromTransformMatrix(result);
+        
+        return t;
+    }
+    
+    public void setWorldBindPose(Matrix4f worldBindPose) {
+        if (cachedWorldBindPose != null) {
+            if (!cachedWorldBindPose.equals(worldBindPose)) {
+                throw new UnsupportedOperationException("Bind poses don't match");
+            }
+        }
+        
+        cachedWorldBindPose = worldBindPose;
+        
+        this.jmeWorldBindPose = new Transform();
+        this.jmeWorldBindPose.setTranslation(worldBindPose.toTranslationVector());
+        this.jmeWorldBindPose.setRotation(worldBindPose.toRotationQuat());
+        this.jmeWorldBindPose.setScale(worldBindPose.toScaleVector());
+        
+        System.out.println("\tBind Pose for " + getName());
+        System.out.println(jmeWorldBindPose);
+        
+        float[] angles = new float[3];
+        jmeWorldBindPose.getRotation().toAngles(angles);
+        System.out.println("Angles: " + angles[0] * FastMath.RAD_TO_DEG + ", " + 
+                                        angles[1] * FastMath.RAD_TO_DEG + ", " + 
+                                        angles[2] * FastMath.RAD_TO_DEG);
+    }
+    
+    public void updateWorldTransforms(Transform jmeParentNodeTransform, Transform parentBindPose) {
+        Transform fbxLocalTransform = computeFbxLocalTransform();
+        jmeLocalNodeTransform.set(fbxLocalTransform);
+        
+        if (jmeParentNodeTransform != null) {
+            jmeParentNodeTransform = jmeParentNodeTransform.clone();
+            switch (inheritMode) {
+                case NoParentScale:
+                case ScaleAfterChildRotation:
+                case ScaleBeforeChildRotation:
+                    jmeWorldNodeTransform.set(jmeLocalNodeTransform);
+                    jmeWorldNodeTransform.combineWithParent(jmeParentNodeTransform);
+                    break;
+            }
+        } else {
+            jmeWorldNodeTransform.set(jmeLocalNodeTransform);
+        }
+        
+        if (jmeWorldBindPose != null) {
+            jmeLocalBindPose = new Transform();
+            
+            // Need to derive local bind pose from world bind pose
+            // (this is to be expected for FBX limbs)
+            jmeLocalBindPose.set(jmeWorldBindPose);
+            jmeLocalBindPose.combineWithParent(parentBindPose.invert());
+            
+            // Its somewhat odd for the transforms to differ ...
+            System.out.println("Bind Pose for: " + getName());
+            if (!jmeLocalBindPose.equals(jmeLocalNodeTransform)) {
+                System.out.println("Local Bind: " + jmeLocalBindPose);
+                System.out.println("Local Trans: " + jmeLocalNodeTransform);
+            }
+            if (!jmeWorldBindPose.equals(jmeWorldNodeTransform)) {
+                System.out.println("World Bind: " + jmeWorldBindPose);
+                System.out.println("World Trans: " + jmeWorldNodeTransform);
+            }
+        } else {
+            // World pose derived from local transforms
+            // (this is to be expected for FBX nodes)
+            jmeLocalBindPose = new Transform();
+            jmeWorldBindPose = new Transform();
+            
+            jmeLocalBindPose.set(jmeLocalNodeTransform);
+            if (parentBindPose != null) {
+                jmeWorldBindPose.set(jmeLocalNodeTransform);
+                jmeWorldBindPose.combineWithParent(parentBindPose);
+            } else {
+                jmeWorldBindPose.set(jmeWorldNodeTransform);
+            }
+        }
+        
+        for (FbxNode child : children) {
+            child.updateWorldTransforms(jmeWorldNodeTransform, jmeWorldBindPose);
+        }
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        
+        Vector3f localTranslation = new Vector3f();
+        Quaternion localRotation = new Quaternion();
+        Vector3f localScale = new Vector3f(Vector3f.UNIT_XYZ);
+        Quaternion preRotation = new Quaternion();
+        
+        for (FbxElement e2 : element.getFbxProperties()) {
+            String propName = (String) e2.properties.get(0);
+            String type = (String) e2.properties.get(3);
+            if (propName.equals("Lcl Translation")) {
+                double x = (Double) e2.properties.get(4);
+                double y = (Double) e2.properties.get(5);
+                double z = (Double) e2.properties.get(6);
+                localTranslation.set((float) x, (float) y, (float) z); //.divideLocal(unitSize);
+            } else if (propName.equals("Lcl Rotation")) {
+                double x = (Double) e2.properties.get(4);
+                double y = (Double) e2.properties.get(5);
+                double z = (Double) e2.properties.get(6);
+                localRotation.fromAngles((float) x * FastMath.DEG_TO_RAD, (float) y * FastMath.DEG_TO_RAD, (float) z * FastMath.DEG_TO_RAD);
+            } else if (propName.equals("Lcl Scaling")) {
+                double x = (Double) e2.properties.get(4);
+                double y = (Double) e2.properties.get(5);
+                double z = (Double) e2.properties.get(6);
+                localScale.set((float) x, (float) y, (float) z); //.multLocal(unitSize);
+            } else if (propName.equals("PreRotation")) {
+                double x = (Double) e2.properties.get(4);
+                double y = (Double) e2.properties.get(5);
+                double z = (Double) e2.properties.get(6);
+                preRotation.set(FbxNodeUtil.quatFromBoneAngles((float) x * FastMath.DEG_TO_RAD, (float) y * FastMath.DEG_TO_RAD, (float) z * FastMath.DEG_TO_RAD));
+            } else if (propName.equals("InheritType")) {
+                int inheritType = (Integer) e2.properties.get(4);
+                inheritMode = InheritMode.values()[inheritType];
+            } else if (propName.equals("Visibility")) {
+                visibility = (Double) e2.properties.get(4);
+            } else if (type.contains("U")) {
+                String userDataKey = (String) e2.properties.get(0);
+                String userDataType = (String) e2.properties.get(1);
+                Object userDataValue;
+                
+                if (userDataType.equals("KString")) {
+                    userDataValue = (String) e2.properties.get(4);
+                } else if (userDataType.equals("int")) {
+                    userDataValue = (Integer) e2.properties.get(4);
+                } else if (userDataType.equals("double")) {
+                    // NOTE: jME3 does not support doubles in UserData.
+                    //       Need to convert to float.
+                    userDataValue = ((Double) e2.properties.get(4)).floatValue();
+                } else if (userDataType.equals("Vector")) {
+                    float x = ((Double) e2.properties.get(4)).floatValue();
+                    float y = ((Double) e2.properties.get(5)).floatValue();
+                    float z = ((Double) e2.properties.get(6)).floatValue();
+                    userDataValue = new Vector3f(x, y, z);
+                } else {
+                    logger.log(Level.WARNING, "Unsupported user data type: {0}. Ignoring.", userDataType);
+                    continue;
+                }
+                
+                userData.put(userDataKey, userDataValue);
+            }
+        }
+        
+        // Create local transform
+        // TODO: take into account Maya-style transforms (pre / post rotation ..)
+        jmeLocalNodeTransform.setTranslation(localTranslation);
+        jmeLocalNodeTransform.setRotation(localRotation);
+        jmeLocalNodeTransform.setScale(localScale);
+        
+        if (element.getChildById("Vertices") != null) {
+            // This is an old-style FBX 6.1
+            // Meshes could be embedded inside the node..
+            
+            // Inject the mesh into ourselves..
+            FbxMesh mesh = new FbxMesh(assetManager, sceneFolderName);
+            mesh.fromElement(element);
+            connectObject(mesh);
+        }
+    }
+    
+    private Spatial tryCreateGeometry(int materialIndex, Mesh jmeMesh, boolean single) {
+        // Map meshes without material indices to material 0.
+        if (materialIndex == -1) {
+            materialIndex = 0;
+        }
+        
+        Material jmeMat;
+        if (materialIndex >= materials.size()) {
+            // Material index does not exist. Create default material.
+            jmeMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+            jmeMat.setReceivesShadows(true);
+        } else {
+            FbxMaterial fbxMat = materials.get(materialIndex);
+            jmeMat = fbxMat.getJmeObject();
+        }
+        
+        String geomName = getName();
+        if (single) {
+            geomName += "-submesh";
+        } else {
+            geomName += "-mat-" + materialIndex + "-submesh";
+        }
+        Spatial spatial = new Geometry(geomName, jmeMesh);
+        spatial.setMaterial(jmeMat);
+        if (jmeMat.isTransparent()) {
+            spatial.setQueueBucket(Bucket.Transparent);
+        }
+        if (jmeMat.isReceivesShadows()) {
+            spatial.setShadowMode(ShadowMode.Receive);
+        }
+        spatial.updateModelBound();
+        return spatial;
+    }
+    
+    /**
+     * If this geometry node is deformed by a skeleton, this
+     * returns the node containing the skeleton.
+     * 
+     * In jME3, a mesh can be deformed by a skeleton only if it is 
+     * a child of the node containing the skeleton. However, this
+     * is not a requirement in FBX, so we have to modify the scene graph
+     * of the loaded model to adjust for this.
+     * This happens automatically in 
+     * {@link #createScene(com.jme3.scene.plugins.fbx.node.FbxNode)}.
+     * 
+     * @return The model this node would like to be a child of, or null
+     * if no preferred parent.
+     */
+    public FbxNode getPreferredParent() {
+        if (!(nodeAttribute instanceof FbxMesh)) {
+            return null;
+        }
+        
+        FbxMesh fbxMesh = (FbxMesh) nodeAttribute;
+        FbxSkinDeformer deformer = fbxMesh.getSkinDeformer();
+        FbxNode preferredParent = null;
+        
+        if (deformer != null) {
+            for (FbxCluster cluster : deformer.getJmeObject()) {
+                FbxLimbNode limb = cluster.getLimb();
+                if (preferredParent == null) {
+                    preferredParent = limb.getSkeletonHolder();
+                } else if (preferredParent != limb.getSkeletonHolder()) {
+                    logger.log(Level.WARNING, "A mesh is being deformed by multiple skeletons. "
+                                            + "Only one skeleton will work, ignoring other skeletons.");
+                }
+            }
+        }
+        
+        return preferredParent;
+    }
+    
+    @Override
+    public Spatial toJmeObject() {
+        Spatial spatial;
+        
+        if (nodeAttribute instanceof FbxMesh) {
+            FbxMesh fbxMesh = (FbxMesh) nodeAttribute;
+            IntMap<Mesh> jmeMeshes = fbxMesh.getJmeObject();
+            
+            if (jmeMeshes == null || jmeMeshes.size() == 0) {
+                // No meshes found on FBXMesh (??)
+                logger.log(Level.WARNING, "No meshes could be loaded. Creating empty node.");
+                spatial = new Node(getName() + "-node");
+            } else {
+                // Multiple jME3 geometries required for a single FBXMesh.
+                String nodeName;
+                if (children.isEmpty()) {
+                    nodeName = getName() + "-mesh";
+                } else {
+                    nodeName = getName() + "-node";
+                }
+                Node node = new Node(nodeName);
+                boolean singleMesh = jmeMeshes.size() == 1;
+                for (IntMap.Entry<Mesh> meshInfo : jmeMeshes) {
+                    node.attachChild(tryCreateGeometry(meshInfo.getKey(), meshInfo.getValue(), singleMesh));
+                }
+                spatial = node;
+            }
+        } else {
+            if (nodeAttribute != null) {
+                // Just specifies that this is a "null" node.
+                nodeAttribute.getJmeObject();
+            }
+            
+            // TODO: handle other node attribute types.
+            //       right now everything we don't know about gets converted
+            //       to jME3 Node.
+            spatial = new Node(getName() + "-node");
+        }
+        
+        if (!children.isEmpty()) {
+            // Check uniform scale.
+            // Although, if inheritType is 0 (eInheritRrSs)
+            // it might not be a problem.
+            Vector3f localScale = jmeLocalNodeTransform.getScale();
+            if (!FastMath.approximateEquals(localScale.x, localScale.y) || 
+                !FastMath.approximateEquals(localScale.x, localScale.z)) {
+                logger.log(Level.WARNING, "Non-uniform scale detected on parent node. " +
+                                          "The model may appear distorted.");
+            }
+        }
+        
+        spatial.setLocalTransform(jmeLocalNodeTransform);
+        
+        if (visibility == 0.0) {
+            spatial.setCullHint(CullHint.Always);
+        }
+        
+        for (Map.Entry<String, Object> userDataEntry : userData.entrySet()) {
+            spatial.setUserData(userDataEntry.getKey(), userDataEntry.getValue());
+        }
+        
+        return spatial;
+    }
+    
+    /**
+     * Create jME3 Skeleton objects on the scene. 
+     * 
+     * Goes through the scene graph and finds limbs that are 
+     * attached to FBX nodes, then creates a Skeleton on the node
+     * based on the child limbs.
+     * 
+     * Must be called prior to calling 
+     * {@link #createScene(com.jme3.scene.plugins.fbx.node.FbxNode)}.
+     * 
+     * @param fbxNode The root FBX node.
+     */
+    public static void createSkeletons(FbxNode fbxNode) {
+        boolean createSkeleton = false;
+        for (FbxNode fbxChild : fbxNode.children) {
+            if (fbxChild instanceof FbxLimbNode) {
+                createSkeleton = true;
+            } else {
+                createSkeletons(fbxChild);
+            }
+        }
+        if (createSkeleton) {
+            if (fbxNode.skeleton != null) {
+                throw new UnsupportedOperationException();
+            }
+            fbxNode.skeleton = FbxLimbNode.createSkeleton(fbxNode);
+            System.out.println("created skeleton: " + fbxNode.skeleton);
+        }
+    }
+    
+    private static void relocateSpatial(Spatial spatial, 
+                                        Transform originalWorldTransform, Transform newWorldTransform) {
+        Transform localTransform = new Transform();
+        localTransform.set(originalWorldTransform);
+        localTransform.combineWithParent(newWorldTransform.invert());
+        spatial.setLocalTransform(localTransform);
+    }
+    
+    public static Spatial createScene(FbxNode fbxNode) {
+        Spatial jmeSpatial = fbxNode.getJmeObject();
+        
+        if (jmeSpatial instanceof Node) {
+            // Attach children to Node
+            Node jmeNode = (Node) jmeSpatial;
+            for (FbxNode fbxChild : fbxNode.children) {
+                if (!(fbxChild instanceof FbxLimbNode)) {
+                    createScene(fbxChild);
+                    
+                    FbxNode preferredParent = fbxChild.getPreferredParent();
+                    Spatial jmeChild = fbxChild.getJmeObject();
+                    if (preferredParent != null) {
+                        System.out.println("Preferred parent for " + fbxChild + " is " + preferredParent);
+                        
+                        Node jmePreferredParent = (Node) preferredParent.getJmeObject();
+                        relocateSpatial(jmeChild, fbxChild.jmeWorldNodeTransform, 
+                                                  preferredParent.jmeWorldNodeTransform);
+                        jmePreferredParent.attachChild(jmeChild);
+                    } else {
+                        jmeNode.attachChild(jmeChild);
+                    }
+                }
+            }
+        }
+        
+        if (fbxNode.skeleton != null) { 
+            jmeSpatial.addControl(new AnimControl(fbxNode.skeleton));
+            jmeSpatial.addControl(new SkeletonControl(fbxNode.skeleton));
+            
+            SkeletonDebugger sd = new SkeletonDebugger("debug", fbxNode.skeleton);
+            Material mat = new Material(fbxNode.assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+            mat.getAdditionalRenderState().setWireframe(true);
+            mat.getAdditionalRenderState().setDepthTest(false);
+            mat.setColor("Color", ColorRGBA.Green);
+            sd.setMaterial(mat);
+            
+            ((Node)jmeSpatial).attachChild(sd);
+        }
+        
+        return jmeSpatial;
+    }
+    
+//    public SceneLoader.Limb toLimb() {
+//        SceneLoader.Limb limb = new SceneLoader.Limb();
+//        limb.name = getName();
+//        Quaternion rotation = preRotation.mult(localRotation);
+//        limb.bindTransform = new Transform(localTranslation, rotation, localScale);
+//        return limb;
+//    }
+    
+    public Skeleton getJmeSkeleton() {
+        return skeleton;
+    }
+    
+    public List<FbxNode> getChildren() { 
+        return children;
+    }
+    
+    @Override
+    public void connectObject(FbxObject object) {
+        if (object instanceof FbxNode) {
+            // Scene Graph Object
+            FbxNode childNode = (FbxNode) object;
+            if (childNode.parent != null) {
+                throw new IllegalStateException("Cannot attach " + childNode
+                                              + " to " + this + ". It is already "
+                                              + "attached to " + childNode.parent);
+            }
+            childNode.parent = this;
+            children.add(childNode);
+        } else if (object instanceof FbxNodeAttribute) {
+            // Node Attribute
+            if (nodeAttribute != null) { 
+                throw new IllegalStateException("An FBXNodeAttribute (" + nodeAttribute + ")" +
+                                                " is already attached to " + this + ". " +
+                                                "Only one attribute allowed per node.");
+            }
+            
+            nodeAttribute = (FbxNodeAttribute) object;
+            if (nodeAttribute instanceof FbxNullAttribute) {
+                nodeAttribute.getJmeObject();
+            }
+        } else if (object instanceof FbxMaterial) {
+            materials.add((FbxMaterial) object);
+        } else if (object instanceof FbxImage || object instanceof FbxTexture) {
+            // Ignore - attaching textures to nodes is legacy feature.
+        } else {
+            unsupportedConnectObject(object);
+        }
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        // Only allowed to connect local transform properties to object
+        // (FbxAnimCurveNode)
+        if (object instanceof FbxAnimCurveNode) {
+            FbxAnimCurveNode curveNode = (FbxAnimCurveNode) object;
+            if (property.equals("Lcl Translation")
+                    || property.equals("Lcl Rotation")
+                    || property.equals("Lcl Scaling")) {
+                
+                List<FbxAnimCurveNode> curveNodes = propertyToAnimCurveMap.get(property);
+                if (curveNodes == null) {
+                    curveNodes = new ArrayList<FbxAnimCurveNode>();
+                    curveNodes.add(curveNode);
+                    propertyToAnimCurveMap.put(property, curveNodes);
+                }
+                curveNodes.add(curveNode);
+                
+                // Make sure the curve knows about it animating
+                // this node as well. 
+                curveNode.addInfluencedNode(this, property);
+            } else {
+                logger.log(Level.WARNING, "Animating the property ''{0}'' is not "
+                                        + "supported. Ignoring.", property);
+            }
+        } else {
+            unsupportedConnectObjectProperty(object, property);
+        }
+    }
+    
+}

+ 41 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNodeAttribute.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.node;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+
+public abstract class FbxNodeAttribute<JT> extends FbxObject<JT> {
+    public FbxNodeAttribute(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+}

+ 61 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNodeUtil.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.node;
+
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+
+public class FbxNodeUtil {
+    public static Quaternion quatFromBoneAngles(float xAngle, float yAngle, float zAngle) {
+        float angle;
+        float sinY, sinZ, sinX, cosY, cosZ, cosX;
+        angle = zAngle * 0.5f;
+        sinZ = FastMath.sin(angle);
+        cosZ = FastMath.cos(angle);
+        angle = yAngle * 0.5f;
+        sinY = FastMath.sin(angle);
+        cosY = FastMath.cos(angle);
+        angle = xAngle * 0.5f;
+        sinX = FastMath.sin(angle);
+        cosX = FastMath.cos(angle);
+        float cosYXcosZ = cosY * cosZ;
+        float sinYXsinZ = sinY * sinZ;
+        float cosYXsinZ = cosY * sinZ;
+        float sinYXcosZ = sinY * cosZ;
+        // For some reason bone space is differ, this is modified formulas
+        float w = (cosYXcosZ * cosX + sinYXsinZ * sinX);
+        float x = (cosYXcosZ * sinX - sinYXsinZ * cosX);
+        float y = (sinYXcosZ * cosX + cosYXsinZ * sinX);
+        float z = (cosYXsinZ * cosX - sinYXcosZ * sinX);
+        return new Quaternion(x, y, z, w).normalizeLocal();
+    }
+}

+ 59 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNullAttribute.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.node;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+
+public class FbxNullAttribute extends FbxNodeAttribute<Object> {
+
+    public FbxNullAttribute(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+
+    @Override
+    protected Object toJmeObject() {
+        // No data in a "Null" attribute.
+        return new Object();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        throw new UnsupportedOperationException();
+    }
+    
+}

+ 45 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxRootNode.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.node;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxId;
+
+public class FbxRootNode extends FbxNode {
+    public FbxRootNode(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+        this.id = FbxId.ROOT;
+        this.className = "Model";
+        this.name = "Scene";
+        this.subclassName = "";
+    }
+}

+ 144 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/obj/FbxObject.java

@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.obj;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.file.FbxId;
+import java.util.logging.Logger;
+
+public abstract class FbxObject<JT> {
+    
+    private static final Logger logger = Logger.getLogger(FbxObject.class.getName());
+    
+    protected AssetManager assetManager;
+    protected String sceneFolderName;
+    
+    protected FbxId id;
+    protected String name;
+    protected String className;
+    protected String subclassName;
+    
+    protected JT jmeObject; // lazily initialized
+    
+    protected FbxObject(AssetManager assetManager, String sceneFolderName) {
+        this.assetManager = assetManager;
+        this.sceneFolderName = sceneFolderName;
+    }
+    
+    public FbxId getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getClassName() {
+        return className;
+    }
+
+    public String getSubclassName() {
+        return subclassName;
+    }
+    
+    public String getFullClassName() { 
+        if (subclassName.equals("")) {
+            return className;
+        } else {
+            return subclassName + " : " + className;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return name + " (" + id + ")";
+    }
+
+    protected void fromElement(FbxElement element) {
+        id = FbxId.getObjectId(element);
+        String nameAndClass;
+        if (element.propertiesTypes.length == 3) {
+            nameAndClass = (String) element.properties.get(1);
+            subclassName = (String) element.properties.get(2);
+        } else if (element.propertiesTypes.length == 2) {
+            nameAndClass = (String) element.properties.get(0);
+            subclassName = (String) element.properties.get(1);
+        } else {
+            throw new UnsupportedOperationException("This is not an FBX object: " + element.id);
+        }
+        
+        int splitter = nameAndClass.indexOf("\u0000\u0001");
+        
+        if (splitter != -1) {
+            name      = nameAndClass.substring(0, splitter);
+            className = nameAndClass.substring(splitter + 2);
+        } else {
+            name      = nameAndClass;
+            className = null;
+        }
+    }
+    
+    public final JT getJmeObject() {
+        if (jmeObject == null) {
+            jmeObject = toJmeObject();
+            if (jmeObject == null) {
+                throw new UnsupportedOperationException("FBX object subclass "
+                                                      + "failed to resolve to a jME3 object");
+            }
+        }
+        return jmeObject;
+    }
+    
+    public final boolean isJmeObjectCreated() {
+        return jmeObject != null;
+    }
+    
+    protected final void unsupportedConnectObject(FbxObject object) {
+        throw new IllegalArgumentException("Cannot attach objects of this class (" + 
+                                            object.getFullClassName() + 
+                                            ") to " + getClass().getSimpleName());
+    }
+    
+    protected final void unsupportedConnectObjectProperty(FbxObject object, String property) {
+        throw new IllegalArgumentException("Cannot attach objects of this class (" + 
+                                            object.getFullClassName() + 
+                                            ") to property " + getClass().getSimpleName() + 
+                                            "[\"" + property + "\"]");
+    }
+    
+    protected abstract JT toJmeObject();
+    
+    public abstract void connectObject(FbxObject object);
+    
+    public abstract void connectObjectProperty(FbxObject object, String property);
+}

+ 209 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/obj/FbxObjectFactory.java

@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.obj;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimCurve;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimCurveNode;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimLayer;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimStack;
+import com.jme3.scene.plugins.fbx.anim.FbxBindPose;
+import com.jme3.scene.plugins.fbx.anim.FbxCluster;
+import com.jme3.scene.plugins.fbx.anim.FbxLimbNode;
+import com.jme3.scene.plugins.fbx.anim.FbxSkinDeformer;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.material.FbxImage;
+import com.jme3.scene.plugins.fbx.material.FbxMaterial;
+import com.jme3.scene.plugins.fbx.material.FbxTexture;
+import com.jme3.scene.plugins.fbx.mesh.FbxMesh;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import com.jme3.scene.plugins.fbx.node.FbxNullAttribute;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Responsible for producing FBX objects given an FBXElement.
+ */
+public final class FbxObjectFactory {
+    
+    private static final Logger logger = Logger.getLogger(FbxObjectFactory.class.getName());
+    
+    private static Class<? extends FbxObject> getImplementingClass(String elementName, String subclassName) {
+        if (elementName.equals("NodeAttribute")) {
+            if (subclassName.equals("Root")) {
+                // Root of skeleton, may not actually be set.
+                return FbxNullAttribute.class;
+            } else if (subclassName.equals("LimbNode")) {
+                // Specifies some limb attributes, optional.
+                return FbxNullAttribute.class;
+            } else if (subclassName.equals("Null")) {
+                // An "Empty" or "Node" without any specific behavior.
+                return FbxNullAttribute.class;
+            } else if (subclassName.equals("IKEffector") ||
+                       subclassName.equals("FKEffector")) {
+                // jME3 does not support IK.
+                return FbxNullAttribute.class;
+            } else {
+                // NodeAttribute - Unknown
+                logger.log(Level.WARNING, "Unknown object subclass: {0}. Ignoring.", subclassName);
+                return FbxUnknownObject.class;
+            }
+        } else if (elementName.equals("Geometry") && subclassName.equals("Mesh")) {
+            // NodeAttribute - Mesh Data
+            return FbxMesh.class;
+        } else if (elementName.equals("Model")) {
+            // Scene Graph Node
+            // Determine specific subclass (e.g. Mesh, Null, or LimbNode?)
+            if (subclassName.equals("LimbNode")) {
+                return FbxLimbNode.class; // Child Bone of Skeleton?
+            } else {
+                return FbxNode.class;
+            }
+        } else if (elementName.equals("Pose")) {
+            if (subclassName.equals("BindPose")) {
+                // Bind Pose Information
+                return FbxBindPose.class;
+            } else {
+                // Rest Pose Information
+                // OR
+                // Other Data (???)
+                logger.log(Level.WARNING, "Unknown object subclass: {0}. Ignoring.", subclassName);
+                return FbxUnknownObject.class;
+            }
+        } else if (elementName.equals("Material")) {
+            return FbxMaterial.class;
+        } else if (elementName.equals("Deformer")) {
+            // Deformer
+            if (subclassName.equals("Skin")) {
+                // FBXSkinDeformer (mapping between FBXMesh & FBXClusters)
+                return FbxSkinDeformer.class;
+            } else if (subclassName.equals("Cluster")) {
+                // Cluster (aka mapping between FBXMesh vertices & weights for bone)
+                return FbxCluster.class;
+            } else {
+                logger.log(Level.WARNING, "Unknown deformer subclass: {0}. Ignoring.", subclassName);
+                return FbxUnknownObject.class;
+            }
+        } else if (elementName.equals("Video")) {
+            if (subclassName.equals("Clip")) {
+                return FbxImage.class;
+            } else {
+                logger.log(Level.WARNING, "Unknown object subclass: {0}. Ignoring.", subclassName);
+                return FbxUnknownObject.class;
+            }
+        } else if (elementName.equals("Texture")) {
+            return FbxTexture.class;
+        } else if (elementName.equals("AnimationStack")) {
+            // AnimationStack (jME Animation)
+            return FbxAnimStack.class;
+        } else if (elementName.equals("AnimationLayer")) {
+            // AnimationLayer (for blended animation - not supported)
+            return FbxAnimLayer.class;
+        } else if (elementName.equals("AnimationCurveNode")) {
+            // AnimationCurveNode
+            return FbxAnimCurveNode.class;
+        } else if (elementName.equals("AnimationCurve")) {
+            // AnimationCurve (Data)
+            return FbxAnimCurve.class;
+        } else if (elementName.equals("SceneInfo")) {
+            // Old-style FBX 6.1 uses this. Nothing useful here.
+            return FbxUnknownObject.class;
+        } else {
+            logger.log(Level.WARNING, "Unknown object class: {0}. Ignoring.", elementName);
+            return FbxUnknownObject.class;
+        }
+    }
+    
+    /**
+     * Automatically create an FBXObject by inspecting its class / subclass
+     * properties.
+     * 
+     * @param element The element from which to create an object.
+     * @param assetManager AssetManager to load dependent resources
+     * @param sceneFolderName Folder relative to which resources shall be loaded
+     * @return The object, or null if not supported (?)
+     */
+    public static FbxObject createObject(FbxElement element, AssetManager assetManager, String sceneFolderName) {
+        String elementName = element.id;
+        String subclassName;
+        
+        if (element.propertiesTypes.length == 3) {
+            // FBX 7.x (all objects start with Long ID)
+            subclassName = (String) element.properties.get(2);
+        } else if (element.propertiesTypes.length == 2) {
+            // FBX 6.x (objects only have name and subclass)
+            subclassName = (String) element.properties.get(1);
+        } else {
+            // Not an object or invalid data.
+            return null;
+        }
+        
+        Class<? extends FbxObject> javaFbxClass = getImplementingClass(elementName, subclassName);
+        
+        if (javaFbxClass != null) {
+            try {
+                // This object is supported by FBX importer, create new instance.
+                // Import the data into the object from the element, then return it.
+                Constructor<? extends FbxObject> ctor = javaFbxClass.getConstructor(AssetManager.class, String.class);
+                FbxObject obj = ctor.newInstance(assetManager, sceneFolderName);
+                obj.fromElement(element);
+                
+                String subClassName = elementName + ", " + subclassName;
+                if (obj.assetManager == null) {
+                    throw new IllegalStateException("FBXObject subclass (" + subClassName + 
+                                                    ") forgot to call super() in their constructor");
+                } else if (obj.className == null) {
+                    throw new IllegalStateException("FBXObject subclass (" + subClassName + 
+                                                    ") forgot to call super.fromElement() in their fromElement() implementation");
+                }
+                return obj;
+            } catch (InvocationTargetException ex) {
+                // Programmer error.
+                throw new IllegalStateException(ex);
+            } catch (NoSuchMethodException ex) {
+                // Programmer error.
+                throw new IllegalStateException(ex);
+            } catch (InstantiationException ex) {
+                // Programmer error.
+                throw new IllegalStateException(ex);
+            } catch (IllegalAccessException ex) {
+                // Programmer error.
+                throw new IllegalStateException(ex);
+            }
+        }
+        
+        // Not supported object.
+        return null;
+    }
+}

+ 54 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/obj/FbxUnknownObject.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.fbx.obj;
+
+import com.jme3.asset.AssetManager;
+
+public class FbxUnknownObject extends FbxObject<Void> {
+
+    public FbxUnknownObject(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    protected Void toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+    }
+}

+ 89 - 0
jme3-plugins/src/main/java/com/jme3/scene/plugins/IrBoneWeightIndex.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins;
+
+public class IrBoneWeightIndex implements Cloneable, Comparable<IrBoneWeightIndex> {
+    
+    int boneIndex;
+    float boneWeight;
+
+    public IrBoneWeightIndex(int boneIndex, float boneWeight) {
+        this.boneIndex = boneIndex;
+        this.boneWeight = boneWeight;
+    }
+
+    @Override
+    public Object clone() {
+        try {
+            return super.clone();
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError(ex);
+        }
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 23 * hash + this.boneIndex;
+        hash = 23 * hash + Float.floatToIntBits(this.boneWeight);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final IrBoneWeightIndex other = (IrBoneWeightIndex) obj;
+        if (this.boneIndex != other.boneIndex) {
+            return false;
+        }
+        if (Float.floatToIntBits(this.boneWeight) != Float.floatToIntBits(other.boneWeight)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int compareTo(IrBoneWeightIndex o) {
+        if (boneWeight < o.boneWeight) {
+            return 1;
+        } else if (boneWeight > o.boneWeight) {
+            return -1;
+        } else {
+            return 0;
+        }
+    }
+}

+ 46 - 0
jme3-plugins/src/main/java/com/jme3/scene/plugins/IrMesh.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins;
+
+public class IrMesh {
+    
+    public IrPolygon[] polygons;
+    
+    public IrMesh deepClone() {
+        IrMesh m = new IrMesh();
+        m.polygons = new IrPolygon[polygons.length];
+        for (int i = 0; i < polygons.length; i++) {
+            m.polygons[i] = polygons[i].deepClone();
+        }
+        return m;
+    }
+}

+ 46 - 0
jme3-plugins/src/main/java/com/jme3/scene/plugins/IrPolygon.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins;
+
+public class IrPolygon {
+    
+    public IrVertex[] vertices;
+    
+    public IrPolygon deepClone() {
+        IrPolygon p = new IrPolygon();
+        p.vertices = new IrVertex[vertices.length];
+        for (int i = 0; i < vertices.length; i++) {
+            p.vertices[i] = vertices[i].deepClone();
+        }
+        return p;
+    }
+}

+ 400 - 0
jme3-plugins/src/main/java/com/jme3/scene/plugins/IrUtils.java

@@ -0,0 +1,400 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins;
+
+import com.jme3.math.Vector4f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.scene.mesh.IndexIntBuffer;
+import com.jme3.scene.mesh.IndexShortBuffer;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.IntMap;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public final class IrUtils {
+    
+    private static final Logger logger = Logger.getLogger(IrUtils.class.getName());
+    
+    private IrUtils() { }
+    
+    private static IrPolygon[] quadToTri(IrPolygon quad) {
+        if (quad.vertices.length == 3) {
+            throw new IllegalStateException("Already a triangle");
+        } 
+        
+        IrPolygon[] t = new IrPolygon[]{ new IrPolygon(), new IrPolygon() };
+        t[0].vertices = new IrVertex[3];
+        t[1].vertices = new IrVertex[3];
+        
+        IrVertex v0 = quad.vertices[0];
+        IrVertex v1 = quad.vertices[1];
+        IrVertex v2 = quad.vertices[2];
+        IrVertex v3 = quad.vertices[3];
+        
+        // find the pair of verticies that is closest to each over
+        // v0 and v2
+        // OR
+        // v1 and v3
+        float d1 = v0.pos.distanceSquared(v2.pos);
+        float d2 = v1.pos.distanceSquared(v3.pos);
+        if (d1 < d2) {
+            // v0 is close to v2
+            // put an edge in v0, v2
+            t[0].vertices[0] = v0;
+            t[0].vertices[1] = v1;
+            t[0].vertices[2] = v3;
+
+            t[1].vertices[0] = v1;
+            t[1].vertices[1] = v2;
+            t[1].vertices[2] = v3;
+        } else {
+            // put an edge in v1, v3
+            t[0].vertices[0] = v0;
+            t[0].vertices[1] = v1;
+            t[0].vertices[2] = v2;
+
+            t[1].vertices[0] = v0;
+            t[1].vertices[1] = v2;
+            t[1].vertices[2] = v3;
+        }
+        
+        return t;
+    }
+    
+    /**
+     * Applies smoothing groups to vertex normals.
+     */
+    public static IrMesh applySmoothingGroups(IrMesh mesh) {
+        return null;
+    }
+    
+    private static void toTangentsWithParity(IrVertex vertex) {
+        if (vertex.tang != null && vertex.bitang != null) {
+            float wCoord = vertex.norm.cross(vertex.tang).dot(vertex.bitang) < 0f ? -1f : 1f;
+            vertex.tang4d = new Vector4f(vertex.tang.x, vertex.tang.y, vertex.tang.z, wCoord);
+            vertex.tang = null;
+            vertex.bitang = null;
+        }
+    }
+    
+    public static void toTangentsWithParity(IrMesh mesh) {
+        for (IrPolygon polygon : mesh.polygons) {
+            for (IrVertex vertex : polygon.vertices) {
+                toTangentsWithParity(vertex);
+            }
+        }
+    }
+    
+    private static void trimBoneWeights(IrVertex vertex) {
+        if (vertex.boneWeightsIndices == null) {
+            return;
+        }
+        
+        IrBoneWeightIndex[] boneWeightsIndices = vertex.boneWeightsIndices;
+        
+        if (boneWeightsIndices.length <= 4) {
+            return;
+        }
+        
+        // Sort by weight
+        boneWeightsIndices = Arrays.copyOf(boneWeightsIndices, boneWeightsIndices.length);
+        Arrays.sort(boneWeightsIndices);
+        
+        // Trim to four weights at most
+        boneWeightsIndices = Arrays.copyOf(boneWeightsIndices, 4);
+        
+        // Renormalize weights
+        float sum = 0;
+        
+        for (int i = 0; i < boneWeightsIndices.length; i++) {
+            sum += boneWeightsIndices[i].boneWeight;
+        }
+        
+        if (sum != 1f) {
+            float sumToB = sum == 0 ? 0 : 1f / sum;
+            for (int i = 0; i < boneWeightsIndices.length; i++) {
+                IrBoneWeightIndex original = boneWeightsIndices[i];
+                boneWeightsIndices[i] = new IrBoneWeightIndex(original.boneIndex, original.boneWeight * sumToB);
+            }
+        }
+        
+        vertex.boneWeightsIndices = boneWeightsIndices;
+    }
+    
+    /**
+     * Removes low bone weights from mesh, leaving only 4 bone weights at max.
+     */
+    public static void trimBoneWeights(IrMesh mesh) {
+        for (IrPolygon polygon : mesh.polygons) {
+            for (IrVertex vertex : polygon.vertices) {
+                trimBoneWeights(vertex);
+            }
+        }
+    }
+    
+    /**
+     * Convert mesh from quads / triangles to triangles only.
+     */
+    public static void triangulate(IrMesh mesh) {
+        List<IrPolygon> newPolygons = new ArrayList<IrPolygon>(mesh.polygons.length);
+        for (IrPolygon inputPoly : mesh.polygons) {
+            if (inputPoly.vertices.length == 4) {
+                IrPolygon[] tris = quadToTri(inputPoly);
+                newPolygons.add(tris[0]);
+                newPolygons.add(tris[1]);
+            } else if (inputPoly.vertices.length == 3) {
+                newPolygons.add(inputPoly);
+            } else {
+                // N-gon. We have to ignore it..
+                logger.log(Level.WARNING, "N-gon encountered, ignoring. "
+                                        + "The mesh may not appear correctly. "
+                                        + "Triangulate your model prior to export.");
+            }
+        }
+        mesh.polygons = new IrPolygon[newPolygons.size()];
+        newPolygons.toArray(mesh.polygons);
+    }
+    
+    /**
+     * Separate mesh with multiple materials into multiple meshes each with 
+     * one material each.
+     * 
+     * Polygons without a material will be added to key = -1.
+     */
+    public static IntMap<IrMesh> splitByMaterial(IrMesh mesh) {
+        IntMap<List<IrPolygon>> materialToPolyList = new IntMap<List<IrPolygon>>();
+        for (IrPolygon polygon : mesh.polygons) {
+            int materialIndex = -1;
+            for (IrVertex vertex : polygon.vertices) {
+                if (vertex.material == null) {
+                    continue;
+                }
+                if (materialIndex == -1) {
+                    materialIndex = vertex.material;
+                } else if (materialIndex != vertex.material) {
+                    throw new UnsupportedOperationException("Multiple materials "
+                                                 + "assigned to the same polygon");
+                }
+            }
+            List<IrPolygon> polyList = materialToPolyList.get(materialIndex);
+            if (polyList == null) {
+                polyList = new ArrayList<IrPolygon>();
+                materialToPolyList.put(materialIndex, polyList);
+            }
+            polyList.add(polygon);
+        }
+        IntMap<IrMesh> materialToMesh = new IntMap<IrMesh>();
+        for (IntMap.Entry<List<IrPolygon>> entry : materialToPolyList) {
+            int key = entry.getKey();
+            List<IrPolygon> polygons = entry.getValue();
+            if (polygons.size() > 0) {
+                IrMesh newMesh = new IrMesh();
+                newMesh.polygons = new IrPolygon[polygons.size()];
+                polygons.toArray(newMesh.polygons);
+                materialToMesh.put(key, newMesh);
+            }
+        }
+        return materialToMesh;
+    }
+     
+    /**
+     * Convert IrMesh to jME3 mesh.
+     */
+    public static Mesh convertIrMeshToJmeMesh(IrMesh mesh) {
+        Map<IrVertex, Integer> vertexToVertexIndex = new HashMap<IrVertex, Integer>();
+        List<IrVertex> vertices = new ArrayList<IrVertex>();
+        List<Integer> indexes = new ArrayList<Integer>();
+        
+        int vertexIndex = 0;
+        for (IrPolygon polygon : mesh.polygons) {
+            if (polygon.vertices.length != 3) {
+                throw new UnsupportedOperationException("IrMesh must be triangulated first");
+            }
+            for (IrVertex vertex : polygon.vertices) {
+                // Is this vertex already indexed?
+                Integer existingIndex = vertexToVertexIndex.get(vertex);
+                if (existingIndex == null) {
+                    // Not indexed yet, allocate index.
+                    indexes.add(vertexIndex);
+                    vertexToVertexIndex.put(vertex, vertexIndex);
+                    vertices.add(vertex);
+                    vertexIndex++;
+                } else {
+                    // Index already allocated for this vertex, reuse it.
+                    indexes.add(existingIndex);
+                }
+            }
+        }
+        
+        Mesh jmeMesh = new Mesh();
+        jmeMesh.setMode(Mesh.Mode.Triangles);
+        
+        FloatBuffer posBuf = null;
+        FloatBuffer normBuf = null;
+        FloatBuffer tangBuf = null;
+        FloatBuffer uv0Buf = null;
+        FloatBuffer uv1Buf = null;
+        ByteBuffer colorBuf = null;
+        ByteBuffer boneIndices = null;
+        FloatBuffer boneWeights = null;
+        IndexBuffer indexBuf = null;
+        
+        IrVertex inspectionVertex = vertices.get(0);
+        if (inspectionVertex.pos != null) {
+            posBuf = BufferUtils.createVector3Buffer(vertices.size());
+            jmeMesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
+        }
+        if (inspectionVertex.norm != null) {
+            normBuf = BufferUtils.createVector3Buffer(vertices.size());
+            jmeMesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
+        }
+        if (inspectionVertex.tang4d != null) {
+            tangBuf = BufferUtils.createFloatBuffer(vertices.size() * 4);
+            jmeMesh.setBuffer(VertexBuffer.Type.Tangent, 4, tangBuf);
+        }
+        if (inspectionVertex.tang != null || inspectionVertex.bitang != null) {
+            throw new IllegalStateException("Mesh is using 3D tangents, must be converted to 4D tangents first.");
+        }
+        if (inspectionVertex.uv0 != null) {
+            uv0Buf = BufferUtils.createVector2Buffer(vertices.size());
+            jmeMesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv0Buf);
+        }
+        if (inspectionVertex.uv1 != null) {
+            uv1Buf = BufferUtils.createVector2Buffer(vertices.size());
+            jmeMesh.setBuffer(VertexBuffer.Type.TexCoord2, 2, uv1Buf);
+        }
+        if (inspectionVertex.color != null) {
+            colorBuf = BufferUtils.createByteBuffer(vertices.size() * 4);
+            jmeMesh.setBuffer(VertexBuffer.Type.Color, 4, colorBuf);
+            jmeMesh.getBuffer(VertexBuffer.Type.Color).setNormalized(true);
+        }
+        if (inspectionVertex.boneWeightsIndices != null) {
+            boneIndices = BufferUtils.createByteBuffer(vertices.size() * 4);
+            boneWeights = BufferUtils.createFloatBuffer(vertices.size() * 4);
+            jmeMesh.setBuffer(VertexBuffer.Type.BoneIndex,  4, boneIndices);
+            jmeMesh.setBuffer(VertexBuffer.Type.BoneWeight, 4, boneWeights);
+            
+            //creating empty buffers for HW skinning 
+            //the buffers will be setup if ever used.
+            VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight);
+            VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex);
+            //setting usage to cpuOnly so that the buffer is not send empty to the GPU
+            indicesHW.setUsage(VertexBuffer.Usage.CpuOnly);
+            weightsHW.setUsage(VertexBuffer.Usage.CpuOnly);
+            
+            jmeMesh.setBuffer(weightsHW);
+            jmeMesh.setBuffer(indicesHW);
+        }
+        if (vertices.size() >= 65536) {
+            // too many verticies: use intbuffer instead of shortbuffer
+            IntBuffer ib = BufferUtils.createIntBuffer(indexes.size());
+            jmeMesh.setBuffer(VertexBuffer.Type.Index, 3, ib);
+            indexBuf = new IndexIntBuffer(ib);
+        } else {
+            ShortBuffer sb = BufferUtils.createShortBuffer(indexes.size());
+            jmeMesh.setBuffer(VertexBuffer.Type.Index, 3, sb);
+            indexBuf = new IndexShortBuffer(sb);
+        }
+        
+        jmeMesh.setStatic();
+        
+        int maxBonesPerVertex = -1;
+        
+        for (IrVertex vertex : vertices) {
+            if (posBuf != null) {
+                posBuf.put(vertex.pos.x).put(vertex.pos.y).put(vertex.pos.z);
+            }
+            if (normBuf != null) {
+                normBuf.put(vertex.norm.x).put(vertex.norm.y).put(vertex.norm.z);
+            }
+            if (tangBuf != null) {
+                tangBuf.put(vertex.tang4d.x).put(vertex.tang4d.y).put(vertex.tang4d.z).put(vertex.tang4d.w);
+            }
+            if (uv0Buf != null) {
+                uv0Buf.put(vertex.uv0.x).put(vertex.uv0.y);
+            }
+            if (uv1Buf != null) {
+                uv1Buf.put(vertex.uv1.x).put(vertex.uv1.y);
+            }
+            if (colorBuf != null) {
+                colorBuf.putInt(vertex.color.asIntABGR());
+            }
+            if (boneIndices != null) {
+                if (vertex.boneWeightsIndices != null) {
+                    if (vertex.boneWeightsIndices.length > 4) {
+                        throw new UnsupportedOperationException("Mesh uses more than 4 weights per bone. " +
+                                                                "Call trimBoneWeights() to allieviate this");
+                    }
+                    for (int i = 0; i < vertex.boneWeightsIndices.length; i++) {
+                        boneIndices.put((byte) (vertex.boneWeightsIndices[i].boneIndex & 0xFF));
+                        boneWeights.put(vertex.boneWeightsIndices[i].boneWeight);
+                    }
+                    for (int i = 0; i < 4 - vertex.boneWeightsIndices.length; i++) {
+                        boneIndices.put((byte)0);
+                        boneWeights.put(0f);
+                    }
+                } else {
+                    boneIndices.putInt(0);
+                    boneWeights.put(0f).put(0f).put(0f).put(0f);
+                }
+                
+                maxBonesPerVertex = Math.max(maxBonesPerVertex, vertex.boneWeightsIndices.length);
+            }
+        }
+        
+        for (int i = 0; i < indexes.size(); i++) {
+            indexBuf.put(i, indexes.get(i));
+        }
+        
+        jmeMesh.updateCounts();
+        jmeMesh.updateBound();
+        
+        if (boneIndices != null) {
+            jmeMesh.setMaxNumWeights(maxBonesPerVertex);
+            jmeMesh.prepareForAnim(true);
+            jmeMesh.generateBindPose(true);
+        }
+        
+        return jmeMesh;
+    }
+}

+ 170 - 0
jme3-plugins/src/main/java/com/jme3/scene/plugins/IrVertex.java

@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins;
+
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
+import java.util.Arrays;
+
+public class IrVertex implements Cloneable {
+
+    public Vector3f pos;
+    public Vector3f norm;
+    public Vector4f tang4d;
+    public Vector3f tang;
+    public Vector3f bitang;
+    public Vector2f uv0;
+    public Vector2f uv1;
+    public ColorRGBA color;
+    public Integer material;
+    public Integer smoothing;
+    public IrBoneWeightIndex[] boneWeightsIndices;
+
+    public IrVertex deepClone() {
+        IrVertex v = new IrVertex();
+        v.pos    = pos != null ? pos.clone() : null;
+        v.norm   = norm != null ? norm.clone() : null;
+        v.tang4d = tang4d != null ? tang4d.clone() : null;
+        v.tang = tang != null ? tang.clone() : null;
+        v.bitang = bitang != null ? bitang.clone() : null;
+        v.uv0 = uv0 != null ? uv0.clone() : null;
+        v.uv1 = uv1 != null ? uv1.clone() : null;
+        v.color = color != null ? color.clone() : null;
+        v.material = material;
+        v.smoothing = smoothing;
+        if (boneWeightsIndices != null) {
+            v.boneWeightsIndices = new IrBoneWeightIndex[boneWeightsIndices.length];
+            for (int i = 0; i < boneWeightsIndices.length; i++) {
+                v.boneWeightsIndices[i] = (IrBoneWeightIndex) boneWeightsIndices[i].clone();
+            }
+        }
+        return v;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 73 * hash + (this.pos != null ? this.pos.hashCode() : 0);
+        hash = 73 * hash + (this.norm != null ? this.norm.hashCode() : 0);
+        hash = 73 * hash + (this.tang4d != null ? this.tang4d.hashCode() : 0);
+        hash = 73 * hash + (this.tang != null ? this.tang.hashCode() : 0);
+        hash = 73 * hash + (this.uv0 != null ? this.uv0.hashCode() : 0);
+        hash = 73 * hash + (this.uv1 != null ? this.uv1.hashCode() : 0);
+        hash = 73 * hash + (this.color != null ? this.color.hashCode() : 0);
+        hash = 73 * hash + (this.material != null ? this.material.hashCode() : 0);
+        hash = 73 * hash + (this.smoothing != null ? this.smoothing.hashCode() : 0);
+        hash = 73 * hash + Arrays.deepHashCode(this.boneWeightsIndices);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final IrVertex other = (IrVertex) obj;
+        if (this.pos != other.pos && (this.pos == null || !this.pos.equals(other.pos))) {
+            return false;
+        }
+        if (this.norm != other.norm && (this.norm == null || !this.norm.equals(other.norm))) {
+            return false;
+        }
+        if (this.tang4d != other.tang4d && (this.tang4d == null || !this.tang4d.equals(other.tang4d))) {
+            return false;
+        }
+        if (this.tang != other.tang && (this.tang == null || !this.tang.equals(other.tang))) {
+            return false;
+        }
+        if (this.uv0 != other.uv0 && (this.uv0 == null || !this.uv0.equals(other.uv0))) {
+            return false;
+        }
+        if (this.uv1 != other.uv1 && (this.uv1 == null || !this.uv1.equals(other.uv1))) {
+            return false;
+        }
+        if (this.color != other.color && (this.color == null || !this.color.equals(other.color))) {
+            return false;
+        }
+        if (this.material != other.material && (this.material == null || !this.material.equals(other.material))) {
+            return false;
+        }
+        if (this.smoothing != other.smoothing && (this.smoothing == null || !this.smoothing.equals(other.smoothing))) {
+            return false;
+        }
+        if (!Arrays.deepEquals(this.boneWeightsIndices, other.boneWeightsIndices)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Vertex { ");
+
+        if (pos != null) {
+            sb.append("pos=").append(pos).append(", ");
+        }
+        if (norm != null) {
+            sb.append("norm=").append(pos).append(", ");
+        }
+        if (tang != null) {
+            sb.append("tang=").append(pos).append(", ");
+        }
+        if (uv0 != null) {
+            sb.append("uv0=").append(pos).append(", ");
+        }
+        if (uv1 != null) {
+            sb.append("uv1=").append(pos).append(", ");
+        }
+        if (color != null) {
+            sb.append("color=").append(pos).append(", ");
+        }
+        if (material != null) {
+            sb.append("material=").append(pos).append(", ");
+        }
+        if (smoothing != null) {
+            sb.append("smoothing=").append(pos).append(", ");
+        }
+
+        if (sb.toString().endsWith(", ")) {
+            sb.delete(sb.length() - 2, sb.length());
+        }
+
+        sb.append(" }");
+        return sb.toString();
+    }
+}