Sfoglia il codice sorgente

Implemented FBX file loader

tort32 11 anni fa
parent
commit
e5002c1428

+ 1 - 0
jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg

@@ -22,3 +22,4 @@ LOADER com.jme3.scene.plugins.ogre.MaterialLoader : material
 LOADER com.jme3.scene.plugins.ogre.SceneLoader : scene
 LOADER com.jme3.scene.plugins.blender.BlenderModelLoader : blend
 LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, glsl, glsllib
+LOADER com.jme3.scene.plugins.fbx.SceneLoader : fbx

+ 1 - 0
jme3-plugins/build.gradle

@@ -6,6 +6,7 @@ sourceSets {
     main {
         java {
             srcDir 'src/ogre/java'
+            srcDir 'src/fbx/java'
             srcDir 'src/xml/java'
         }
     }

+ 77 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/AnimationList.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2009-2014 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 java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Defines animations set that will be created while loading FBX scene
+ * <p>Animation <code>name</code> is using to access animation via {@link AnimControl}.<br>
+ * <code>firstFrame</code> and <code>lastFrame</code> defines animation time interval.<br>
+ * Use <code>layerName</code> also to define source animation layer in the case of multiple layers in the scene.<br>
+ * Skeletal animations will be created if only scene contain skeletal bones</p>
+ */
+public class AnimationList {
+	
+	List<AnimInverval> list = new ArrayList<AnimInverval>();
+	
+	/**
+	 * Use in the case of multiple animation layers in FBX asset
+	 * @param name - animation name to assess via {@link AnimControl}
+	 * @param layerName - source layer
+	 */
+	public void add(String name, int firstFrame, int lastFrame) {
+		add(name, null, firstFrame, lastFrame);
+	}
+	
+	/**
+	 * Use in the case of multiple animation layers in FBX asset
+	 * @param name - animation name to assess via {@link AnimControl}
+	 * @param layerName - source layer
+	 */
+	public void add(String name, String layerName, int firstFrame, int lastFrame) {
+		AnimInverval cue = new AnimInverval();
+		cue.name = name;
+		cue.layerName = layerName;
+		cue.firstFrame = firstFrame;
+		cue.lastFrame = lastFrame;
+		list.add(cue);
+	}
+	
+	static class AnimInverval {
+		String name;
+		String layerName;
+		int firstFrame;
+		int lastFrame;
+	}
+}

+ 77 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/ContentTextureKey.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2009-2014 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.asset.TextureKey;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+
+import java.io.IOException;
+
+/**
+ * Used to load textures from image binary content.
+ * <p>Filename is required to acquire proper type asset loader according to extension.</p>
+ */
+public class ContentTextureKey extends TextureKey {
+	
+	private byte[] content;
+	
+	public ContentTextureKey(String name, byte[] content) {
+		super(name);
+		this.content = content;
+	}
+	
+	public byte[] getContent() {
+		return content;
+	}
+	
+	@Override
+	public String toString() {
+		return super.toString() + " " + content.length + " bytes";
+	}
+	
+	@Override
+	public void write(JmeExporter ex) throws IOException {
+		super.write(ex);
+		OutputCapsule oc = ex.getCapsule(this);
+		oc.write(content, "content", new byte[0]);
+	}
+	
+	@Override
+	public void read(JmeImporter im) throws IOException {
+		super.read(im);
+		InputCapsule ic = im.getCapsule(this);
+		content = ic.readByteArray("content", new byte[0]);
+	}
+}

+ 89 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/ContentTextureLocator.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2009-2014 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.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLocator;
+import com.jme3.asset.AssetManager;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Used to locate a resource based on a {@link ContentTextureKey}.
+ */
+public class ContentTextureLocator implements AssetLocator {
+	
+	private static final Logger logger = Logger.getLogger(ContentTextureLocator.class.getName());
+	
+	@Override
+	public void setRootPath(String rootPath) {
+	}
+	
+	@SuppressWarnings("rawtypes")
+	@Override
+	public AssetInfo locate(AssetManager manager, AssetKey key) {
+		if(key instanceof ContentTextureKey) {
+			String name = key.getName();
+			byte[] content = ((ContentTextureKey) key).getContent();
+			if(content != null) {
+				return new ContentAssetInfo(manager, key, content);
+			} else {
+				logger.log(Level.WARNING, "No content for " + name);
+				return null;
+			}
+		} else {
+			logger.log(Level.SEVERE, "AssetKey should be TextureContentKey instance");
+			return null;
+		}
+	}
+	
+	private class ContentAssetInfo extends AssetInfo {
+		
+		private InputStream stream;
+		
+		@SuppressWarnings("rawtypes")
+		public ContentAssetInfo(AssetManager assetManager, AssetKey key, byte[] content) {
+			super(assetManager, key);
+			this.stream = (content != null) ? new ByteArrayInputStream(content) : null;
+		}
+		
+		@Override
+		public InputStream openStream() {
+			return stream;
+		}
+	}
+	
+}

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

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2009-2014 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.asset.ModelKey;
+
+public class SceneKey extends ModelKey {
+	
+	private final AnimationList animList;
+	
+	public SceneKey(String name) {
+		super(name);
+		this.animList = null;
+	}
+	
+	public SceneKey(String name, AnimationList animationList) {
+		super(name);
+		this.animList = animationList;
+	}
+	
+	public AnimationList getAnimations() {
+		return this.animList;
+	}
+	
+}

+ 1586 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/SceneLoader.java

@@ -0,0 +1,1586 @@
+/*
+ * Copyright (c) 2009-2014 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 java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+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.SkeletonControl;
+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.material.Material;
+import com.jme3.material.RenderState.BlendMode;
+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.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.Mesh.Mode;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.scene.plugins.fbx.AnimationList.AnimInverval;
+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.texture.Image;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
+import com.jme3.util.BufferUtils;
+
+/**
+ * FBX file format loader
+ * <p> Loads scene meshes, materials, textures, skeleton and skeletal animation.
+ * Multiple animations can be defined with {@link AnimationList} passing into {@link SceneKey}.</p>
+ * 
+ * @author Aleksandra Menshchikova
+ */
+public class SceneLoader implements AssetLoader {
+	
+	private static final Logger logger = Logger.getLogger(SceneLoader.class.getName());
+	
+	private AssetManager assetManager;
+	private SceneKey key;
+	
+	private String sceneName;
+	private String sceneFilename;
+	private String sceneFolderName;
+	private float unitSize;
+	private float animFrameRate;
+	private final double secondsPerUnit = 1 / 46186158000d; // Animation speed factor
+	
+	// Loaded objects data
+	private Map<Long, MeshData> meshDataMap = new HashMap<Long, MeshData>();
+	private Map<Long, MaterialData> matDataMap = new HashMap<Long, MaterialData>();
+	private Map<Long, TextureData> texDataMap = new HashMap<Long, TextureData>();
+	private Map<Long, ImageData> imgDataMap = new HashMap<Long, ImageData>(); // Video clips
+	private Map<Long, ModelData> modelDataMap = new HashMap<Long, ModelData>(); // Mesh nodes and limb nodes
+	private Map<Long, BindPoseData> poseDataMap = new HashMap<Long, BindPoseData>(); // Node bind poses
+	private Map<Long, SkinData> skinMap = new HashMap<Long, SkinData>(); // Skin for bone clusters
+	private Map<Long, ClusterData> clusterMap = new HashMap<Long, ClusterData>(); // Bone skinning cluster
+	private Map<Long, AnimCurveData> acurveMap = new HashMap<Long, AnimCurveData>(); // Animation curves
+	private Map<Long, AnimNode> anodeMap = new HashMap<Long, AnimNode>(); // Animation nodes
+	private Map<Long, AnimLayer> alayerMap = new HashMap<Long, AnimLayer>(); // Amination layers
+	private Map<Long, List<Long>> refMap = new HashMap<Long, List<Long>>(); // Object links
+	private Map<Long, List<PropertyLink>> propMap = new HashMap<Long, List<PropertyLink>>(); // Property links
+	
+	// Scene objects
+	private Map<Long, Node> modelMap = new HashMap<Long, Node>(); // Mesh nodes
+	private Map<Long, Limb> limbMap = new HashMap<Long, Limb>(); // Bones
+	private Map<Long, BindPose> bindMap = new HashMap<Long, BindPose>(); // Node bind poses
+	private Map<Long, Geometry> geomMap = new HashMap<Long, Geometry>(); // Mesh geometries
+	private Map<Long, Material> matMap = new HashMap<Long, Material>();
+	private Map<Long, Texture> texMap = new HashMap<Long, Texture>();
+	private Map<Long, Image> imgMap = new HashMap<Long, Image>();
+	private Skeleton skeleton;
+	private AnimControl animControl;
+	
+	@Override
+	public Object load(AssetInfo assetInfo) throws IOException {
+		this.assetManager = assetInfo.getManager();
+		AssetKey<?> assetKey = assetInfo.getKey();
+		if(assetKey instanceof SceneKey)
+			key = (SceneKey) assetKey;
+		else
+			throw new AssetLoadException("Invalid asset key");
+		InputStream stream = assetInfo.openStream();
+		Node sceneNode = null;
+		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();
+			loadScene(stream);
+			sceneNode = linkScene();
+		} finally {
+			releaseObjects();
+			if(stream != null) {
+				stream.close();
+			}
+		}
+		return sceneNode;
+	}
+	
+	private void reset() {
+		unitSize = 1;
+		animFrameRate = 30;
+	}
+	
+	private void loadScene(InputStream stream) throws IOException {
+		logger.log(Level.FINE, "Loading scene {0}", sceneFilename);
+		long startTime = System.currentTimeMillis();
+		FBXFile scene = FBXReader.readFBX(stream);
+		for(FBXElement e : scene.rootElements) {
+			if(e.id.equals("GlobalSettings"))
+				loadGlobalSettings(e);
+			else if(e.id.equals("Objects"))
+				loadObjects(e);
+			else if(e.id.equals("Connections"))
+				loadConnections(e);
+		}
+		long estimatedTime = System.currentTimeMillis() - startTime;
+		logger.log(Level.FINE, "Loading done in {0} ms", estimatedTime);
+	}
+	
+	private void loadGlobalSettings(FBXElement element) {
+		for(FBXElement e : element.children) {
+			if(e.id.equals("Properties70")) {
+				for(FBXElement e2 : e.children) {
+					if(e2.id.equals("P")) {
+						String propName = (String) e2.properties.get(0);
+						if(propName.equals("UnitScaleFactor"))
+							this.unitSize = ((Double) e2.properties.get(4)).floatValue();
+						else if(propName.equals("CustomFrameRate")) {
+							float framerate = ((Double) e2.properties.get(4)).floatValue();
+							if(framerate != -1)
+								this.animFrameRate = framerate;
+						}
+					}
+				}
+			}
+		}
+	}
+	
+	private void loadObjects(FBXElement element) {
+		for(FBXElement e : element.children) {
+			if(e.id.equals("Geometry"))
+				loadGeometry(e);
+			else if(e.id.equals("Material"))
+				loadMaterial(e);
+			else if(e.id.equals("Model"))
+				loadModel(e);
+			else if(e.id.equals("Pose"))
+				loadPose(e);
+			else if(e.id.equals("Texture"))
+				loadTexture(e);
+			else if(e.id.equals("Video"))
+				loadImage(e);
+			else if(e.id.equals("Deformer"))
+				loadDeformer(e);
+			else if(e.id.equals("AnimationLayer"))
+				loadAnimLayer(e);
+			else if(e.id.equals("AnimationCurve"))
+				loadAnimCurve(e);
+			else if(e.id.equals("AnimationCurveNode"))
+				loadAnimNode(e);
+		}
+	}
+	
+	private void loadGeometry(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String type = (String) element.properties.get(2);
+		if(type.equals("Mesh")) {
+			MeshData data = new MeshData();
+			for(FBXElement e : element.children) {
+				if(e.id.equals("Vertices"))
+					data.vertices = (double[]) e.properties.get(0);
+				else if(e.id.equals("PolygonVertexIndex"))
+					data.indices = (int[]) e.properties.get(0);
+				// TODO edges are not used now
+				//else if(e.id.equals("Edges"))
+				//	data.edges = (int[]) e.properties.get(0);
+				else if(e.id.equals("LayerElementNormal"))
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("MappingInformationType")) {
+							data.normalsMapping = (String) e2.properties.get(0);
+							if(!data.normalsMapping.equals("ByVertice") && !data.normalsMapping.equals("ByPolygonVertex"))
+								throw new AssetLoadException("Not supported LayerElementNormal.MappingInformationType = " + data.normalsMapping);
+						} else if(e2.id.equals("ReferenceInformationType")) {
+							data.normalsReference = (String) e2.properties.get(0);
+							if(!data.normalsReference.equals("Direct"))
+								throw new AssetLoadException("Not supported LayerElementNormal.ReferenceInformationType = " + data.normalsReference);
+						} else if(e2.id.equals("Normals"))
+							data.normals = (double[]) e2.properties.get(0);
+					}
+				else if(e.id.equals("LayerElementTangent"))
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("MappingInformationType")) {
+							data.tangentsMapping = (String) e2.properties.get(0);
+							if(!data.tangentsMapping.equals("ByVertice") && !data.tangentsMapping.equals("ByPolygonVertex"))
+								throw new AssetLoadException("Not supported LayerElementTangent.MappingInformationType = " + data.tangentsMapping);
+						} else if(e2.id.equals("ReferenceInformationType")) {
+							data.tangentsReference = (String) e2.properties.get(0);
+							if(!data.tangentsReference.equals("Direct"))
+								throw new AssetLoadException("Not supported LayerElementTangent.ReferenceInformationType = " + data.tangentsReference);
+						} else if(e2.id.equals("Tangents"))
+							data.tangents = (double[]) e2.properties.get(0);
+					}
+				else if(e.id.equals("LayerElementBinormal"))
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("MappingInformationType")) {
+							data.binormalsMapping = (String) e2.properties.get(0);
+							if(!data.binormalsMapping.equals("ByVertice") && !data.binormalsMapping.equals("ByPolygonVertex"))
+								throw new AssetLoadException("Not supported LayerElementBinormal.MappingInformationType = " + data.binormalsMapping);
+						} else if(e2.id.equals("ReferenceInformationType")) {
+							data.binormalsReference = (String) e2.properties.get(0);
+							if(!data.binormalsReference.equals("Direct"))
+								throw new AssetLoadException("Not supported LayerElementBinormal.ReferenceInformationType = " + data.binormalsReference);
+						} else if(e2.id.equals("Tangents"))
+							data.binormals = (double[]) e2.properties.get(0);
+					}
+				else if(e.id.equals("LayerElementUV"))
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("MappingInformationType")) {
+							data.uvMapping = (String) e2.properties.get(0);
+							if(!data.uvMapping.equals("ByPolygonVertex"))
+								throw new AssetLoadException("Not supported LayerElementUV.MappingInformationType = " + data.uvMapping);
+						} else if(e2.id.equals("ReferenceInformationType")) {
+							data.uvReference = (String) e2.properties.get(0);
+							if(!data.uvReference.equals("IndexToDirect"))
+								throw new AssetLoadException("Not supported LayerElementUV.ReferenceInformationType = " + data.uvReference);
+						} else if(e2.id.equals("UV"))
+							data.uv = (double[]) e2.properties.get(0);
+						else if(e2.id.equals("UVIndex"))
+							data.uvIndex = (int[]) e2.properties.get(0);
+					}
+				// TODO smoothing is not used now
+				//else if(e.id.equals("LayerElementSmoothing"))
+				//	for(FBXElement e2 : e.children) {
+				//		if(e2.id.equals("MappingInformationType")) {
+				//			data.smoothingMapping = (String) e2.properties.get(0);
+				//			if(!data.smoothingMapping.equals("ByEdge"))
+				//				throw new AssetLoadException("Not supported LayerElementSmoothing.MappingInformationType = " + data.smoothingMapping);
+				//		} else if(e2.id.equals("ReferenceInformationType")) {
+				//			data.smoothingReference = (String) e2.properties.get(0);
+				//			if(!data.smoothingReference.equals("Direct"))
+				//				throw new AssetLoadException("Not supported LayerElementSmoothing.ReferenceInformationType = " + data.smoothingReference);
+				//		} else if(e2.id.equals("Smoothing"))
+				//			data.smoothing = (int[]) e2.properties.get(0);
+				//	}
+				else if(e.id.equals("LayerElementMaterial"))
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("MappingInformationType")) {
+							data.materialsMapping = (String) e2.properties.get(0);
+							if(!data.materialsMapping.equals("AllSame"))
+								throw new AssetLoadException("Not supported LayerElementMaterial.MappingInformationType = " + data.materialsMapping);
+						} else if(e2.id.equals("ReferenceInformationType")) {
+							data.materialsReference = (String) e2.properties.get(0);
+							if(!data.materialsReference.equals("IndexToDirect"))
+								throw new AssetLoadException("Not supported LayerElementMaterial.ReferenceInformationType = " + data.materialsReference);
+						} else if(e2.id.equals("Materials"))
+							data.materials = (int[]) e2.properties.get(0);
+					}
+			}
+			meshDataMap.put(id, data);
+		}
+	}
+	
+	private void loadMaterial(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String path = (String) element.properties.get(1);
+		String type = (String) element.properties.get(2);
+		if(type.equals("")) {
+			MaterialData data = new MaterialData();
+			data.name = path.substring(0, path.indexOf(0));
+			for(FBXElement e : element.children) {
+				if(e.id.equals("ShadingModel")) {
+					data.shadingModel = (String) e.properties.get(0);
+				} else if(e.id.equals("Properties70")) {
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("P")) {
+							String propName = (String) e2.properties.get(0);
+							if(propName.equals("AmbientColor")) {
+								double x = (Double) e2.properties.get(4);
+								double y = (Double) e2.properties.get(5);
+								double z = (Double) e2.properties.get(6);
+								data.ambientColor.set((float) x, (float) y, (float) z);
+							} else if(propName.equals("AmbientFactor")) {
+								double x = (Double) e2.properties.get(4);
+								data.ambientFactor = (float) x;
+							} else if(propName.equals("DiffuseColor")) {
+								double x = (Double) e2.properties.get(4);
+								double y = (Double) e2.properties.get(5);
+								double z = (Double) e2.properties.get(6);
+								data.diffuseColor.set((float) x, (float) y, (float) z);
+							} else if(propName.equals("DiffuseFactor")) {
+								double x = (Double) e2.properties.get(4);
+								data.diffuseFactor = (float) x;
+							} else if(propName.equals("SpecularColor")) {
+								double x = (Double) e2.properties.get(4);
+								double y = (Double) e2.properties.get(5);
+								double z = (Double) e2.properties.get(6);
+								data.specularColor.set((float) x, (float) y, (float) z);
+							} else if(propName.equals("Shininess") || propName.equals("ShininessExponent")) {
+								double x = (Double) e2.properties.get(4);
+								data.shininessExponent = (float) x;
+							}
+						}
+					}
+				}
+			}
+			matDataMap.put(id, data);
+		}
+	}
+	
+	private void loadModel(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String path = (String) element.properties.get(1);
+		String type = (String) element.properties.get(2);
+		ModelData data = new ModelData();
+		data.name = path.substring(0, path.indexOf(0));
+		data.type = type;
+		for(FBXElement e : element.children) {
+			if(e.id.equals("Properties70")) {
+				for(FBXElement e2 : e.children) {
+					if(e2.id.equals("P")) {
+						String propName = (String) e2.properties.get(0);
+						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);
+							data.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);
+							data.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);
+							data.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);
+							data.preRotation = quatFromBoneAngles((float) x * FastMath.DEG_TO_RAD, (float) y * FastMath.DEG_TO_RAD, (float) z * FastMath.DEG_TO_RAD);
+						}
+					}
+				}
+			}
+		}
+		modelDataMap.put(id, data);
+	}
+	
+	private void loadPose(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String path = (String) element.properties.get(1);
+		String type = (String) element.properties.get(2);
+		if(type.equals("BindPose")) {
+			BindPoseData data = new BindPoseData();
+			data.name = path.substring(0, path.indexOf(0));
+			for(FBXElement e : element.children) {
+				if(e.id.equals("PoseNode")) {
+					NodeTransformData item = new NodeTransformData();
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("Node"))
+							item.nodeId = (Long) e2.properties.get(0);
+						else if(e2.id.equals("Matrix"))
+							item.transform = (double[]) e2.properties.get(0);
+					}
+					data.list.add(item);
+				}
+			}
+			poseDataMap.put(id, data);
+		}
+	}
+	
+	private void loadTexture(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String path = (String) element.properties.get(1);
+		String type = (String) element.properties.get(2);
+		if(type.equals("")) {
+			TextureData data = new TextureData();
+			data.name = path.substring(0, path.indexOf(0));
+			for(FBXElement e : element.children) {
+				if(e.id.equals("Type"))
+					data.bindType = (String) e.properties.get(0);
+				else if(e.id.equals("FileName"))
+					data.filename = (String) e.properties.get(0);
+			}
+			texDataMap.put(id, data);
+		}
+	}
+	
+	private void loadImage(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String path = (String) element.properties.get(1);
+		String type = (String) element.properties.get(2);
+		if(type.equals("Clip")) {
+			ImageData data = new ImageData();
+			data.name = path.substring(0, path.indexOf(0));
+			for(FBXElement e : element.children) {
+				if(e.id.equals("Type"))
+					data.type = (String) e.properties.get(0);
+				else if(e.id.equals("Filename"))
+					data.filename = (String) e.properties.get(0);
+				else if(e.id.equals("RelativeFilename"))
+					data.relativeFilename = (String) e.properties.get(0);
+				else if(e.id.equals("Content")) {
+					if(e.properties.size() > 0)
+						data.content = (byte[]) e.properties.get(0);
+				}
+			}
+			imgDataMap.put(id, data);
+		}
+	}
+	
+	private void loadDeformer(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String type = (String) element.properties.get(2);
+		if(type.equals("Skin")) {
+			SkinData skinData = new SkinData();
+			for(FBXElement e : element.children) {
+				if(e.id.equals("SkinningType"))
+					skinData.type = (String) e.properties.get(0);
+			}
+			skinMap.put(id, skinData);
+		} else if(type.equals("Cluster")) {
+			ClusterData clusterData = new ClusterData();
+			for(FBXElement e : element.children) {
+				if(e.id.equals("Indexes"))
+					clusterData.indexes = (int[]) e.properties.get(0);
+				else if(e.id.equals("Weights"))
+					clusterData.weights = (double[]) e.properties.get(0);
+				else if(e.id.equals("Transform"))
+					clusterData.transform = (double[]) e.properties.get(0);
+				else if(e.id.equals("TransformLink"))
+					clusterData.transformLink = (double[]) e.properties.get(0);
+			}
+			clusterMap.put(id, clusterData);
+		}
+	}
+	
+	private void loadAnimLayer(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String path = (String) element.properties.get(1);
+		String type = (String) element.properties.get(2);
+		if(type.equals("")) {
+			AnimLayer layer = new AnimLayer();
+			layer.name = path.substring(0, path.indexOf(0));
+			alayerMap.put(id, layer);
+		}
+	}
+	
+	private void loadAnimCurve(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String type = (String) element.properties.get(2);
+		if(type.equals("")) {
+			AnimCurveData data = new AnimCurveData();
+			for(FBXElement e : element.children) {
+				if(e.id.equals("KeyTime"))
+					data.keyTimes = (long[]) e.properties.get(0);
+				else if(e.id.equals("KeyValueFloat"))
+					data.keyValues = (float[]) e.properties.get(0);
+			}
+			acurveMap.put(id, data);
+		}
+	}
+	
+	private void loadAnimNode(FBXElement element) {
+		long id = (Long) element.properties.get(0);
+		String path = (String) element.properties.get(1);
+		String type = (String) element.properties.get(2);
+		if(type.equals("")) {
+			Double x = null, y = null, z = null;
+			for(FBXElement e : element.children) {
+				if(e.id.equals("Properties70")) {
+					for(FBXElement e2 : e.children) {
+						if(e2.id.equals("P")) {
+							String propName = (String) e2.properties.get(0);
+							if(propName.equals("d|X"))
+								x = (Double) e2.properties.get(4);
+							else if(propName.equals("d|Y"))
+								y = (Double) e2.properties.get(4);
+							else if(propName.equals("d|Z"))
+								z = (Double) e2.properties.get(4);
+						}
+					}
+				}
+			}
+			// Load only T R S curve nodes
+			if(x != null && y != null && z != null) {
+				AnimNode node = new AnimNode();
+				node.value = new Vector3f(x.floatValue(), y.floatValue(), z.floatValue());
+				node.name = path.substring(0, path.indexOf(0));
+				anodeMap.put(id, node);
+			}
+		}
+	}
+	
+	private void loadConnections(FBXElement element) {
+		for(FBXElement e : element.children) {
+			if(e.id.equals("C")) {
+				String type = (String) e.properties.get(0);
+				long objId, refId;
+				if(type.equals("OO")) {
+					objId = (Long) e.properties.get(1);
+					refId = (Long) e.properties.get(2);
+					List<Long> links = refMap.get(objId);
+					if(links == null) {
+						links = new ArrayList<Long>();
+						refMap.put(objId, links);
+					}
+					links.add(refId);
+				} else if(type.equals("OP")) {
+					objId = (Long) e.properties.get(1);
+					refId = (Long) e.properties.get(2);
+					String propName = (String) e.properties.get(3);
+					List<PropertyLink> props = propMap.get(objId);
+					if(props == null) {
+						props = new ArrayList<PropertyLink>();
+						propMap.put(objId, props);
+					}
+					props.add(new PropertyLink(refId, propName));
+				}
+			}
+		}
+	}
+	
+	private Geometry createGeomerty(MeshData data) {
+		Mesh mesh = new Mesh();
+		mesh.setMode(Mode.Triangles);
+		// Since each vertex should contain unique texcoord and normal we should unroll vertex indexing
+		// So we don't use VertexBuffer.Type.Index for elements drawing
+		// Moreover quads should be triangulated (this increases number of vertices)
+		boolean isQuads = false;
+		if(data.indices != null) {
+			data.iCount = data.indices.length;
+			data.srcVertexCount = data.vertices.length / 3;
+			// Indices contains negative numbers to define polygon last index
+			// Check indices strides to be sure we have triangles or quads
+			boolean allTriangles = true;
+			boolean allQads = true;
+			for(int i = 0; i < data.indices.length; ++i) {
+				if(i % 3 == 2) { // Triangle stride
+					if(data.indices[i] >= 0)
+						allTriangles = false;
+				} else {
+					if(data.indices[i] < 0)
+						allTriangles = false;
+				}
+				if(i % 4 == 3) { // Quad stride
+					if(data.indices[i] >= 0)
+						allQads = false;
+				} else {
+					if(data.indices[i] < 0)
+						allQads = false;
+				}
+			}
+			if(allTriangles) {
+				isQuads = false;
+				data.vCount = data.iCount;
+			} else if(allQads) {
+				isQuads = true;
+				data.vCount = 6 * (data.iCount / 4); // Each quad will be splited into two triangles
+			} else
+				throw new AssetLoadException("Unsupported PolygonVertexIndex stride");
+			data.vertexMap = new int[data.vCount];
+			data.indexMap = new int[data.vCount];
+			// Unroll index array into vertex mapping
+			int n = 0;
+			for(int i = 0; i < data.iCount; ++i) {
+				int index = data.indices[i];
+				if(index < 0) {
+					int lastIndex = -(index + 1);
+					if(isQuads) {
+						data.vertexMap[n + 0] = data.indices[i - 3];
+						data.vertexMap[n + 1] = data.indices[i - 2];
+						data.vertexMap[n + 2] = data.indices[i - 1];
+						data.vertexMap[n + 3] = data.indices[i - 3];
+						data.vertexMap[n + 4] = data.indices[i - 1];
+						data.vertexMap[n + 5] = lastIndex;
+						data.indexMap[n + 0] = (i - 3);
+						data.indexMap[n + 1] = (i - 2);
+						data.indexMap[n + 2] = (i - 1);
+						data.indexMap[n + 3] = (i - 3);
+						data.indexMap[n + 4] = (i - 1);
+						data.indexMap[n + 5] = (i - 0);
+						n += 6;
+					} else {
+						data.vertexMap[n + 0] = data.indices[i - 2];
+						data.vertexMap[n + 1] = data.indices[i - 1];
+						data.vertexMap[n + 2] = lastIndex;
+						data.indexMap[n + 0] = (i - 2);
+						data.indexMap[n + 1] = (i - 1);
+						data.indexMap[n + 2] = (i - 0);
+						n += 3;
+					}
+				}
+			}
+			// Build reverse vertex mapping
+			data.reverseVertexMap = new ArrayList<List<Integer>>(data.srcVertexCount);
+			for(int i = 0; i < data.srcVertexCount; ++i)
+				data.reverseVertexMap.add(new ArrayList<Integer>());
+			for(int i = 0; i < data.vCount; ++i) {
+				int index = data.vertexMap[i];
+				data.reverseVertexMap.get(index).add(i);
+			}
+		} else {
+			// Stub for no vertex indexing (direct mapping)
+			data.iCount = data.vCount = data.srcVertexCount;
+			data.vertexMap = new int[data.vCount];
+			data.indexMap = new int[data.vCount];
+			data.reverseVertexMap = new ArrayList<List<Integer>>(data.vCount);
+			for(int i = 0; i < data.vCount; ++i) {
+				data.vertexMap[i] = i;
+				data.indexMap[i] = i;
+				List<Integer> reverseIndices = new ArrayList<Integer>(1);
+				reverseIndices.add(i);
+				data.reverseVertexMap.add(reverseIndices);
+			}
+		}
+		if(data.vertices != null) {
+			// Unroll vertices data array
+			FloatBuffer posBuf = BufferUtils.createFloatBuffer(data.vCount * 3);
+			mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
+			int srcCount = data.vertices.length / 3;
+			for(int i = 0; i < data.vCount; ++i) {
+				int index = data.vertexMap[i];
+				if(index > srcCount)
+					throw new AssetLoadException("Invalid vertex mapping. Unexpected lookup vertex " + index + " from " + srcCount);
+				float x = (float) data.vertices[3 * index + 0] / unitSize;
+				float y = (float) data.vertices[3 * index + 1] / unitSize;
+				float z = (float) data.vertices[3 * index + 2] / unitSize;
+				posBuf.put(x).put(y).put(z);
+			}
+		}
+		if(data.normals != null) {
+			// Unroll normals data array
+			FloatBuffer normBuf = BufferUtils.createFloatBuffer(data.vCount * 3);
+			mesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
+			int[] mapping = null;
+			if(data.normalsMapping.equals("ByVertice"))
+				mapping = data.vertexMap;
+			else if(data.normalsMapping.equals("ByPolygonVertex"))
+				mapping = data.indexMap;
+			int srcCount = data.normals.length / 3;
+			for(int i = 0; i < data.vCount; ++i) {
+				int index = mapping[i];
+				if(index > srcCount)
+					throw new AssetLoadException("Invalid normal mapping. Unexpected lookup normal " + index + " from " + srcCount);
+				float x = (float) data.normals[3 * index + 0];
+				float y = (float) data.normals[3 * index + 1];
+				float z = (float) data.normals[3 * index + 2];
+				normBuf.put(x).put(y).put(z);
+			}
+		}
+		if(data.tangents != null) {
+			// Unroll normals data array
+			FloatBuffer tanBuf = BufferUtils.createFloatBuffer(data.vCount * 4);
+			mesh.setBuffer(VertexBuffer.Type.Tangent, 4, tanBuf);
+			int[] mapping = null;
+			if(data.tangentsMapping.equals("ByVertice"))
+				mapping = data.vertexMap;
+			else if(data.tangentsMapping.equals("ByPolygonVertex"))
+				mapping = data.indexMap;
+			int srcCount = data.tangents.length / 3;
+			for(int i = 0; i < data.vCount; ++i) {
+				int index = mapping[i];
+				if(index > srcCount)
+					throw new AssetLoadException("Invalid tangent mapping. Unexpected lookup tangent " + index + " from " + srcCount);
+				float x = (float) data.tangents[3 * index + 0];
+				float y = (float) data.tangents[3 * index + 1];
+				float z = (float) data.tangents[3 * index + 2];
+				tanBuf.put(x).put(y).put(z).put(-1.0f);
+			}
+		}
+		if(data.binormals != null) {
+			// Unroll normals data array
+			FloatBuffer binormBuf = BufferUtils.createFloatBuffer(data.vCount * 3);
+			mesh.setBuffer(VertexBuffer.Type.Binormal, 3, binormBuf);
+			int[] mapping = null;
+			if(data.binormalsMapping.equals("ByVertice"))
+				mapping = data.vertexMap;
+			else if(data.binormalsMapping.equals("ByPolygonVertex"))
+				mapping = data.indexMap;
+			int srcCount = data.binormals.length / 3;
+			for(int i = 0; i < data.vCount; ++i) {
+				int index = mapping[i];
+				if(index > srcCount)
+					throw new AssetLoadException("Invalid binormal mapping. Unexpected lookup binormal " + index + " from " + srcCount);
+				float x = (float) data.binormals[3 * index + 0];
+				float y = (float) data.binormals[3 * index + 1];
+				float z = (float) data.binormals[3 * index + 2];
+				binormBuf.put(x).put(y).put(z);
+			}
+		}
+		if(data.uv != null) {
+			int[] unIndexMap = data.vertexMap;
+			if(data.uvIndex != null) {
+				int uvIndexSrcCount = data.uvIndex.length;
+				if(uvIndexSrcCount != data.iCount)
+					throw new AssetLoadException("Invalid number of texcoord index data " + uvIndexSrcCount + " expected " + data.iCount);
+				// Unroll UV index array
+				unIndexMap = new int[data.vCount];
+				int n = 0; 
+				for(int i = 0; i < data.iCount; ++i) {
+					int index = data.uvIndex[i];
+					if(isQuads && (i % 4) == 3) {
+						unIndexMap[n + 0] = data.uvIndex[i - 3];
+						unIndexMap[n + 1] = data.uvIndex[i - 1];
+						unIndexMap[n + 2] = index;
+						n += 3;
+					} else {
+						unIndexMap[i] = index;
+					}
+				}
+			}
+			// Unroll UV data array
+			FloatBuffer tcBuf = BufferUtils.createFloatBuffer(data.vCount * 2);
+			mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, tcBuf);
+			int srcCount = data.uv.length / 2;
+			for(int i = 0; i < data.vCount; ++i) {
+				int index = unIndexMap[i];
+				if(index > srcCount)
+					throw new AssetLoadException("Invalid texcoord mapping. Unexpected lookup texcoord " + index + " from " + srcCount);
+				float u = (float) data.uv[2 * index + 0];
+				float v = (float) data.uv[2 * index + 1];
+				tcBuf.put(u).put(v);
+			}
+		}
+		mesh.setStatic();
+		mesh.updateBound();
+		mesh.updateCounts();
+		Geometry geom = new Geometry();
+		geom.setMesh(mesh);
+		return geom;
+	}
+	
+	private Material createMaterial(MaterialData data) {
+		Material m = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+		m.setName(data.name);
+		data.ambientColor.multLocal(data.ambientFactor);
+		data.diffuseColor.multLocal(data.diffuseFactor);
+		data.specularColor.multLocal(data.specularFactor);
+		m.setColor("Ambient", new ColorRGBA(data.ambientColor.x, data.ambientColor.y, data.ambientColor.z, 1));
+		m.setColor("Diffuse", new ColorRGBA(data.diffuseColor.x, data.diffuseColor.y, data.diffuseColor.z, 1));
+		m.setColor("Specular", new ColorRGBA(data.specularColor.x, data.specularColor.y, data.specularColor.z, 1));
+		m.setFloat("Shininess", data.shininessExponent);
+		m.setBoolean("UseMaterialColors", true);
+		m.getAdditionalRenderState().setAlphaTest(true);
+		m.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
+		return m;
+	}
+	
+	private Node createNode(ModelData data) {
+		Node model = new Node(data.name);
+		model.setLocalTranslation(data.localTranslation);
+		model.setLocalRotation(data.localRotation);
+		model.setLocalScale(data.localScale);
+		return model;
+	}
+	
+	private Limb createLimb(ModelData data) {
+		Limb limb = new Limb();
+		limb.name = data.name;
+		Quaternion rotation = data.preRotation.mult(data.localRotation);
+		limb.bindTransform = new Transform(data.localTranslation, rotation, data.localScale);
+		return limb;
+	}
+	
+	private BindPose createPose(BindPoseData data) {
+		BindPose pose = new BindPose();
+		pose.name = data.name;
+		for(NodeTransformData item : data.list) {
+			Transform t = buildTransform(item.transform);
+			//t.getTranslation().divideLocal(unitSize);
+			t.getScale().multLocal(unitSize);
+			pose.nodeTransforms.put(item.nodeId, t);
+		}
+		return pose;
+	}
+	
+	private Texture createTexture(TextureData data) {
+		Texture tex = new Texture2D();
+		tex.setName(data.name);
+		return tex;
+	}
+	
+	private Image createImage(ImageData data) {
+		Image image = null;
+		if(data.filename != null) {
+			// Try load by absolute path
+			File file = new File(data.filename);
+			if(file.exists() && file.isFile()) {
+				File dir = new File(file.getParent());
+				String locatorPath = dir.getAbsolutePath();
+				Texture tex = null;
+				try {
+					assetManager.registerLocator(locatorPath, com.jme3.asset.plugins.FileLocator.class);
+					tex = assetManager.loadTexture(file.getName());
+				} catch(Exception e) {
+				} finally {
+					assetManager.unregisterLocator(locatorPath, com.jme3.asset.plugins.FileLocator.class);
+				}
+				if(tex != null)
+					image = tex.getImage();
+			}
+		}
+		if(image == null && data.relativeFilename != null) {
+			// Try load by relative path
+			File dir = new File(sceneFolderName);
+			String locatorPath = dir.getAbsolutePath();
+			Texture tex = null;
+			try {
+				assetManager.registerLocator(locatorPath, com.jme3.asset.plugins.FileLocator.class);
+				tex = assetManager.loadTexture(data.relativeFilename);
+			} catch(Exception e) {
+			} finally {
+				assetManager.unregisterLocator(locatorPath, com.jme3.asset.plugins.FileLocator.class);
+			}
+			if(tex != null)
+				image = tex.getImage();
+		}
+		if(image == null && data.content != null) {
+			// Try load from content
+			String filename = null;
+			if(data.filename != null)
+				filename = new File(data.filename).getName();
+			if(filename != null && data.relativeFilename != null)
+				filename = data.relativeFilename;
+			// Filename is required to aquire asset loader by extension
+			if(filename != null) {
+				String locatorPath = sceneFilename;
+				filename = sceneFilename + File.separatorChar + filename; // Unique path
+				Texture tex = null;
+				try {
+					assetManager.registerLocator(locatorPath, com.jme3.scene.plugins.fbx.ContentTextureLocator.class);
+					tex = assetManager.loadTexture(new ContentTextureKey(filename, data.content));
+				} catch(Exception e) {
+				} finally {
+					assetManager.unregisterLocator(locatorPath, com.jme3.scene.plugins.fbx.ContentTextureLocator.class);
+				}
+				if(tex != null)
+					image = tex.getImage();
+			}
+		}
+		if(image == null)
+			throw new AssetLoadException("Content not loaded for image " + data.name);
+		return image;
+	}
+	
+	private Transform buildTransform(double[] transform) {
+		float[] m = new float[transform.length];
+		for(int i = 0; i < transform.length; ++i)
+			m[i] = (float) transform[i];
+		Matrix4f matrix = new Matrix4f(m);
+		Vector3f pos = matrix.toTranslationVector();
+		Quaternion rot = matrix.toRotationQuat();
+		Vector3f scale = matrix.toScaleVector();
+		return new Transform(pos, rot, scale);
+	}
+	
+	private 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();
+	}
+	
+	private Node linkScene() {
+		logger.log(Level.FINE, "Linking scene objects");
+		long startTime = System.currentTimeMillis();
+		Node sceneNode = linkSceneNodes();
+		linkMaterials();
+		linkMeshes(sceneNode);
+		linkSkins(sceneNode);
+		linkAnimations();
+		long estimatedTime = System.currentTimeMillis() - startTime;
+		logger.log(Level.FINE, "Linking done in {0} ms", estimatedTime);
+		return sceneNode;
+	}
+	
+	private Node linkSceneNodes() {
+		Node sceneNode = new Node(sceneName + "-scene");
+		// Build mesh nodes
+		for(long nodeId : modelDataMap.keySet()) {
+			ModelData data = modelDataMap.get(nodeId);
+			if(data.type.equals("Mesh")) {
+				Node node = createNode(data);
+				modelMap.put(nodeId, node);
+			}
+		}
+		// Link model nodes into scene
+		for(long modelId : modelMap.keySet()) {
+			List<Long> refs = refMap.get(modelId);
+			if(refs == null)
+				continue;
+			Node model = modelMap.get(modelId);
+			for(long refId : refs) {
+				Node rootNode = (refId != 0) ? modelMap.get(refId) : sceneNode;
+				if(rootNode != null)
+					rootNode.attachChild(model);
+			}
+		}
+		// Build bind poses
+		for(long poseId : poseDataMap.keySet()) {
+			BindPoseData data = poseDataMap.get(poseId);
+			BindPose pose = createPose(data);
+			if(pose != null)
+				bindMap.put(poseId, pose);
+		}
+		// Apply bind poses
+		for(BindPose pose : bindMap.values()) {
+			for(long nodeId : pose.nodeTransforms.keySet()) {
+				Node model = modelMap.get(nodeId);
+				if(model != null) {
+					Transform t = pose.nodeTransforms.get(nodeId);
+					model.setLocalTransform(t);
+				}
+			}
+		}
+		return sceneNode;
+	}
+	
+	private void linkMaterials() {
+		// Build materials
+		for(long matId : matDataMap.keySet()) {
+			MaterialData data = matDataMap.get(matId);
+			Material material = createMaterial(data);
+			if(material != null)
+				matMap.put(matId, material);
+		}
+		// Build textures
+		for(long texId : texDataMap.keySet()) {
+			TextureData data = texDataMap.get(texId);
+			Texture tex = createTexture(data);
+			if(tex != null)
+				texMap.put(texId, tex);
+		}
+		// Build Images
+		for(long imgId : imgDataMap.keySet()) {
+			ImageData data = imgDataMap.get(imgId);
+			Image img = createImage(data);
+			if(img != null)
+				imgMap.put(imgId, img);
+		}
+		// Link images to textures
+		for(long imgId : imgMap.keySet()) {
+			List<Long> refs = refMap.get(imgId);
+			if(refs == null)
+				continue;
+			Image img = imgMap.get(imgId);
+			for(long refId : refs) {
+				Texture tex = texMap.get(refId);
+				if(tex != null)
+					tex.setImage(img);
+			}
+		}
+		// Link textures to material maps
+		for(long texId : texMap.keySet()) {
+			List<PropertyLink> props = propMap.get(texId);
+			if(props == null)
+				continue;
+			Texture tex = texMap.get(texId);
+			for(PropertyLink prop : props) {
+				Material mat = matMap.get(prop.ref);
+				if(mat != null) {
+					if(prop.propName.equals("DiffuseColor")) {
+						mat.setTexture("DiffuseMap", tex);
+						mat.setColor("Diffuse", ColorRGBA.White);
+					} else if(prop.propName.equals("SpecularColor")) {
+						mat.setTexture("SpecularMap", tex);
+						mat.setColor("Specular", ColorRGBA.White);
+					} else if(prop.propName.equals("NormalMap"))
+						mat.setTexture("NormalMap", tex);
+				}
+			}
+		}
+	}
+	
+	private void linkMeshes(Node sceneNode) {
+		// Build meshes
+		for(long meshId : meshDataMap.keySet()) {
+			MeshData data = meshDataMap.get(meshId);
+			Geometry geom = createGeomerty(data);
+			if(geom != null)
+				geomMap.put(meshId, geom);
+		}
+		// Link meshes to models
+		for(long geomId : geomMap.keySet()) {
+			List<Long> refs = refMap.get(geomId);
+			if(refs == null)
+				continue;
+			Geometry geom = geomMap.get(geomId);
+			for(long refId : refs) {
+				Node rootNode = (refId != 0) ? modelMap.get(refId) : sceneNode;
+				if(rootNode != null) {
+					geom.setName(rootNode.getName() + "-mesh");
+					geom.updateModelBound();
+					rootNode.attachChild(geom);
+					break;
+				}
+			}
+		}
+		// Link materials to meshes
+		for(long matId : matMap.keySet()) {
+			List<Long> refs = refMap.get(matId);
+			if(refs == null)
+				continue;
+			Material mat = matMap.get(matId);
+			for(long refId : refs) {
+				Node rootNode = modelMap.get(refId);
+				if(rootNode != null) {
+					for(Spatial child : rootNode.getChildren())
+						child.setMaterial(mat);
+				}
+			}
+		}
+	}
+	
+	private void linkSkins(Node sceneNode) {
+		// Build skeleton limbs
+		for(long nodeId : modelDataMap.keySet()) {
+			ModelData data = modelDataMap.get(nodeId);
+			if(data.type.equals("LimbNode")) {
+				Limb limb = createLimb(data);
+				limbMap.put(nodeId, limb);
+			}
+		}
+		if(limbMap.size() == 0)
+			return;
+		// Build skeleton bones
+		Map<Long, Bone> bones = new HashMap<Long, Bone>();
+		for(long limbId : limbMap.keySet()) {
+			Limb limb = limbMap.get(limbId);
+			Bone bone = new Bone(limb.name);
+			Transform t = limb.bindTransform;
+			bone.setBindTransforms(t.getTranslation(), t.getRotation(), t.getScale());
+			bones.put(limbId, bone);
+		}
+		// Attach bones to roots
+		for(long limbId : limbMap.keySet()) {
+			List<Long> refs = refMap.get(limbId);
+			if(refs == null)
+				continue;
+			// Find root limb
+			long rootLimbId = 0L;
+			for(long refId : refs) {
+				if(limbMap.containsKey(refId)) {
+					rootLimbId = refId;
+					break;
+				}
+			}
+			if(rootLimbId != 0L) {
+				Bone bone = bones.get(limbId);
+				Bone root = bones.get(rootLimbId);
+				root.addChild(bone);
+			}
+		}
+		// Link bone clusters to skin
+		for(long clusterId : clusterMap.keySet()) {
+			List<Long> refs = refMap.get(clusterId);
+			if(refs == null)
+				continue;
+			for(long skinId : refs) {
+				if(skinMap.containsKey(skinId)) {
+					ClusterData data = clusterMap.get(clusterId);
+					data.skinId = skinId;
+					break;
+				}
+			}
+		}
+		// Build the skeleton
+		this.skeleton = new Skeleton(bones.values().toArray(new Bone[0]));
+		skeleton.setBindingPose();
+		for(long limbId : bones.keySet()) {
+			Bone bone = bones.get(limbId);
+			Limb limb = limbMap.get(limbId);
+			limb.boneIndex = skeleton.getBoneIndex(bone);
+		}
+		// Assign bones skinning to meshes
+		for(long skinId : skinMap.keySet()) {
+			// Find mesh by skin
+			Mesh mesh = null;
+			MeshData meshData = null;
+			for(long meshId : refMap.get(skinId)) {
+				Geometry g = geomMap.get(meshId);
+				if(g != null) {
+					meshData = meshDataMap.get(meshId);
+					mesh = g.getMesh();
+					break;
+				}
+			}
+			// Bind skinning indexes and weight
+			if(mesh != null && meshData != null) {
+				// Create bone buffers
+				FloatBuffer boneWeightData = BufferUtils.createFloatBuffer(meshData.vCount * 4);
+				ByteBuffer boneIndicesData = BufferUtils.createByteBuffer(meshData.vCount * 4);
+				mesh.setBuffer(VertexBuffer.Type.BoneWeight, 4, boneWeightData);
+				mesh.setBuffer(VertexBuffer.Type.BoneIndex, 4, boneIndicesData);
+				mesh.getBuffer(VertexBuffer.Type.BoneWeight).setUsage(Usage.CpuOnly);
+				mesh.getBuffer(VertexBuffer.Type.BoneIndex).setUsage(Usage.CpuOnly);
+				VertexBuffer weightsHW = new VertexBuffer(Type.HWBoneWeight);
+				VertexBuffer indicesHW = new VertexBuffer(Type.HWBoneIndex);
+				indicesHW.setUsage(Usage.CpuOnly); // Setting usage to CpuOnly so that the buffer is not send empty to the GPU
+				weightsHW.setUsage(Usage.CpuOnly);
+				mesh.setBuffer(weightsHW);
+				mesh.setBuffer(indicesHW);
+				// Accumulate skin bones influence into mesh buffers
+				boolean bonesLimitExceeded = false;
+				for(long limbId : bones.keySet()) {
+					// Search bone cluster for the given limb and skin
+					ClusterData cluster = null;
+					for(long clusterId : refMap.get(limbId)) {
+						ClusterData data = clusterMap.get(clusterId);
+						if(data != null && data.skinId == skinId) {
+							cluster = data;
+							break;
+						}
+					}
+					if(cluster == null || cluster.indexes == null || cluster.weights == null || cluster.indexes.length != cluster.weights.length)
+						continue;
+					Limb limb = limbMap.get(limbId);
+					if(limb.boneIndex > 255)
+						throw new AssetLoadException("Bone index can't be packed into byte");
+					for(int i = 0; i < cluster.indexes.length; ++i) {
+						int vertexIndex = cluster.indexes[i];
+						if(vertexIndex >= meshData.reverseVertexMap.size())
+							throw new AssetLoadException("Invalid skinning vertex index. Unexpected index lookup " + vertexIndex + " from " + meshData.reverseVertexMap.size());
+						List<Integer> dstVertices = meshData.reverseVertexMap.get(vertexIndex);
+						for(int v : dstVertices) {
+							// Append bone index and weight to vertex
+							int offset;
+							float w = 0;
+							for(offset = v * 4; offset < v * 4 + 4; ++offset) {
+								w = boneWeightData.get(offset);
+								if(w == 0)
+									break;
+							}
+							if(w == 0) {
+								boneWeightData.put(offset, (float) cluster.weights[i]);
+								boneIndicesData.put(offset, (byte) limb.boneIndex);
+							} else {
+								// TODO It would be nice to discard small weight to accumulate more heavy influence
+								bonesLimitExceeded = true;
+							}
+						}
+					}
+				}
+				if(bonesLimitExceeded)
+					logger.log(Level.WARNING, "Skinning support max 4 bone per vertex. Exceeding data will be discarded.");
+				// Postprocess bones weights
+				int maxWeightsPerVert = 0;
+				boneWeightData.rewind();
+				for(int v = 0; v < meshData.vCount; v++) {
+					float w0 = boneWeightData.get();
+					float w1 = boneWeightData.get();
+					float w2 = boneWeightData.get();
+					float w3 = boneWeightData.get();
+					if(w3 != 0) {
+						maxWeightsPerVert = Math.max(maxWeightsPerVert, 4);
+					} else if(w2 != 0) {
+						maxWeightsPerVert = Math.max(maxWeightsPerVert, 3);
+					} else if(w1 != 0) {
+						maxWeightsPerVert = Math.max(maxWeightsPerVert, 2);
+					} else if(w0 != 0) {
+						maxWeightsPerVert = Math.max(maxWeightsPerVert, 1);
+					}
+					float sum = w0 + w1 + w2 + w3;
+					if(sum != 1f) {
+						// normalize weights
+						float mult = (sum != 0) ? (1f / sum) : 0;
+						boneWeightData.position(v * 4);
+						boneWeightData.put(w0 * mult);
+						boneWeightData.put(w1 * mult);
+						boneWeightData.put(w2 * mult);
+						boneWeightData.put(w3 * mult);
+					}
+				}
+				mesh.setMaxNumWeights(maxWeightsPerVert);
+				mesh.generateBindPose(true);
+			}
+		}
+		// Attach controls
+		animControl = new AnimControl(skeleton);
+		sceneNode.addControl(animControl);
+		SkeletonControl control = new SkeletonControl(skeleton);
+		sceneNode.addControl(control);
+	}
+	
+	private void linkAnimations() {
+		if(skeleton == null)
+			return;
+		AnimationList animList = key.getAnimations();
+		if(animList == null || animList.list.size() == 0)
+			return;
+		// Link curves to nodes
+		for(long curveId : acurveMap.keySet()) {
+			List<PropertyLink> props = propMap.get(curveId);
+			if(props == null)
+				continue;
+			AnimCurveData acurve = acurveMap.get(curveId);
+			for(PropertyLink prop : props) {
+				AnimNode anode = anodeMap.get(prop.ref);
+				if(anode != null) {
+					if(prop.propName.equals("d|X"))
+						anode.xCurve = acurve;
+					else if(prop.propName.equals("d|Y"))
+						anode.yCurve = acurve;
+					else if(prop.propName.equals("d|Z"))
+						anode.zCurve = acurve;
+				}
+			}
+		}
+		// Link nodes to layers
+		for(long nodeId : anodeMap.keySet()) {
+			List<Long> refs = refMap.get(nodeId);
+			if(refs == null)
+				continue;
+			for(long layerId : refs) {
+				if(alayerMap.containsKey(layerId)) {
+					AnimNode anode = anodeMap.get(nodeId);
+					anode.layerId = layerId;
+					break;
+				}
+			}
+		}
+		// Extract aminations
+		HashMap<String, Animation> anims = new HashMap<String, Animation>();
+		for(AnimInverval animInfo : animList.list) {
+			float length = (animInfo.lastFrame - animInfo.firstFrame) / this.animFrameRate;
+			float animStart = animInfo.firstFrame / this.animFrameRate;
+			float animStop = animInfo.lastFrame / this.animFrameRate;
+			Animation anim = new Animation(animInfo.name, length);
+			// Search source layer for animation nodes
+			long sourceLayerId = 0L;
+			for(long layerId : alayerMap.keySet()) {
+				AnimLayer layer = alayerMap.get(layerId);
+				if(layer.name.equals(animInfo.layerName)) {
+					sourceLayerId = layerId;
+					break;
+				}
+			}
+			// Assign animation nodes to limbs
+			for(Limb limb : limbMap.values()) {
+				limb.animTranslation = null;
+				limb.animRotation = null;
+				limb.animScale = null;
+			}
+			for(long nodeId : anodeMap.keySet()) {
+				List<PropertyLink> props = propMap.get(nodeId);
+				if(props == null)
+					continue;
+				AnimNode anode = anodeMap.get(nodeId);
+				if(sourceLayerId != 0L && anode.layerId != sourceLayerId)
+					continue; // filter node
+				for(PropertyLink prop : props) {
+					Limb limb = limbMap.get(prop.ref);
+					if(limb != null) {
+						if(prop.propName.equals("Lcl Translation"))
+							limb.animTranslation = anode;
+						else if(prop.propName.equals("Lcl Rotation"))
+							limb.animRotation = anode;
+						else if(prop.propName.equals("Lcl Scaling"))
+							limb.animScale = anode;
+					}
+				}
+			}
+			// Build bone tracks
+			for(Limb limb : limbMap.values()) {
+				long[] keyTimes = null;
+				boolean haveTranslation = (limb.animTranslation != null && limb.animTranslation.xCurve != null && limb.animTranslation.yCurve != null && limb.animTranslation.zCurve != null);
+				boolean haveRotation = (limb.animRotation != null && limb.animRotation.xCurve != null && limb.animRotation.yCurve != null && limb.animRotation.zCurve != null);
+				boolean haveScale = (limb.animScale != null && limb.animScale.xCurve != null && limb.animScale.yCurve != null && limb.animScale.zCurve != null);
+				// Search key time array
+				if(haveTranslation)
+					keyTimes = limb.animTranslation.xCurve.keyTimes;
+				else if(haveRotation)
+					keyTimes = limb.animRotation.xCurve.keyTimes;
+				else if(haveScale)
+					keyTimes = limb.animScale.xCurve.keyTimes;
+				if(keyTimes == null)
+					continue;
+				// Calculate keys interval by animation time interval
+				int firstKey = 0;
+				int lastKey = keyTimes.length - 1;
+				for(int i = 0; i < keyTimes.length; ++i) {
+					float time = (float) (((double) keyTimes[i]) * secondsPerUnit); // Translate into seconds
+					if(time <= animStart)
+						firstKey = i;
+					if(time >= animStop) {
+						lastKey = i;
+						break;
+					}
+				}
+				int keysCount = lastKey - firstKey + 1;
+				if(keysCount <= 0)
+					continue;
+				float[] times = new float[keysCount];
+				Vector3f[] translations = new Vector3f[keysCount];
+				Quaternion[] rotations = new Quaternion[keysCount];
+				Vector3f[] scales = null;
+				// Calculate keyframes times
+				for(int i = 0; i < keysCount; ++i) {
+					int keyIndex = firstKey + i;
+					float time = (float) (((double) keyTimes[keyIndex]) * secondsPerUnit); // Translate into seconds
+					times[i] = time - animStart;
+				}
+				// Load keyframes from animation curves
+				if(haveTranslation) {
+					for(int i = 0; i < keysCount; ++i) {
+						int keyIndex = firstKey + i;
+						float x = limb.animTranslation.xCurve.keyValues[keyIndex] - limb.animTranslation.value.x;
+						float y = limb.animTranslation.yCurve.keyValues[keyIndex] - limb.animTranslation.value.y;
+						float z = limb.animTranslation.zCurve.keyValues[keyIndex] - limb.animTranslation.value.z;
+						translations[i] = new Vector3f(x, y, z).divideLocal(unitSize);
+					}
+				} else {
+					for(int i = 0; i < keysCount; ++i)
+						translations[i] = new Vector3f();
+				}
+				if(haveRotation) {
+					for(int i = 0; i < keysCount; ++i) {
+						int keyIndex = firstKey + i;
+						float x = limb.animRotation.xCurve.keyValues[keyIndex];
+						float y = limb.animRotation.yCurve.keyValues[keyIndex];
+						float z = limb.animRotation.zCurve.keyValues[keyIndex];
+						rotations[i] = new Quaternion().fromAngles(FastMath.DEG_TO_RAD * x, FastMath.DEG_TO_RAD * y, FastMath.DEG_TO_RAD * z);
+					}
+				} else {
+					for(int i = 0; i < keysCount; ++i)
+						rotations[i] = new Quaternion();
+				}
+				if(haveScale) {
+					scales = new Vector3f[keysCount];
+					for(int i = 0; i < keysCount; ++i) {
+						int keyIndex = firstKey + i;
+						float x = limb.animScale.xCurve.keyValues[keyIndex];
+						float y = limb.animScale.yCurve.keyValues[keyIndex];
+						float z = limb.animScale.zCurve.keyValues[keyIndex];
+						scales[i] = new Vector3f(x, y, z);
+					}
+				}
+				BoneTrack track = null;
+				if(haveScale)
+					track = new BoneTrack(limb.boneIndex, times, translations, rotations, scales);
+				else
+					track = new BoneTrack(limb.boneIndex, times, translations, rotations);
+				anim.addTrack(track);
+			}
+			anims.put(anim.getName(), anim);
+		}
+		animControl.setAnimations(anims);
+	}
+	
+	private void releaseObjects() {
+		meshDataMap.clear();
+		matDataMap.clear();
+		texDataMap.clear();
+		imgDataMap.clear();
+		modelDataMap.clear();
+		poseDataMap.clear();
+		skinMap.clear();
+		clusterMap.clear();
+		acurveMap.clear();
+		anodeMap.clear();
+		alayerMap.clear();
+		refMap.clear();
+		propMap.clear();
+		modelMap.clear();
+		limbMap.clear();
+		bindMap.clear();
+		geomMap.clear();
+		matMap.clear();
+		texMap.clear();
+		imgMap.clear();
+		skeleton = null;
+		animControl = null;
+		key = null;
+	}
+	
+	private class MeshData {
+		double[] vertices;
+		int[] indices;
+		int[] edges;
+		String normalsMapping;
+		String normalsReference;
+		double[] normals;
+		String tangentsMapping;
+		String tangentsReference;
+		double[] tangents;
+		String binormalsMapping;
+		String binormalsReference;
+		double[] binormals;
+		String uvMapping;
+		String uvReference;
+		double[] uv;
+		int[] uvIndex;
+		String smoothingMapping;
+		String smoothingReference;
+		int[] smoothing;
+		String materialsMapping;
+		String materialsReference;
+		int[] materials;
+		// Build helping data
+		int iCount;
+		int vCount;
+		int srcVertexCount;
+		int[] vertexMap; // Target vertex -> source vertex
+		List<List<Integer>> reverseVertexMap; // source vertex -> list of target vertices
+		int[] indexMap; // Target vertex -> source index
+	}
+	
+	private class ModelData {
+		String name;
+		String type;
+		Vector3f localTranslation = new Vector3f();
+		Quaternion localRotation = new Quaternion();
+		Vector3f localScale = new Vector3f(Vector3f.UNIT_XYZ);
+		Quaternion preRotation = new Quaternion();
+	}
+	
+	private class NodeTransformData {
+		long nodeId;
+		double[] transform;
+	}
+	
+	private class BindPoseData {
+		String name;
+		List<NodeTransformData> list = new LinkedList<NodeTransformData>();
+	}
+	
+	private class BindPose {
+		String name;
+		Map<Long, Transform> nodeTransforms = new HashMap<Long, Transform>();
+	}
+	
+	private class MaterialData {
+		String name;
+		String shadingModel = "phong";
+		Vector3f ambientColor = new Vector3f(0.2f, 0.2f, 0.2f);
+		float ambientFactor = 1.0f;
+		Vector3f diffuseColor = new Vector3f(0.8f, 0.8f, 0.8f);
+		float diffuseFactor = 1.0f;
+		Vector3f specularColor = new Vector3f(0.2f, 0.2f, 0.2f);
+		float specularFactor = 1.0f;
+		float shininessExponent = 20.0f;
+	}
+	
+	private class TextureData {
+		String name;
+		String bindType;
+		String filename;
+	}
+	
+	private class ImageData {
+		String name;
+		String type;
+		String filename;
+		String relativeFilename;
+		byte[] content;
+	}
+	
+	private class Limb {
+		String name;
+		Transform bindTransform;
+		int boneIndex;
+		AnimNode animTranslation;
+		AnimNode animRotation;
+		AnimNode animScale;
+	}
+	
+	private class ClusterData {
+		int[] indexes;
+		double[] weights;
+		double[] transform;
+		double[] transformLink;
+		long skinId;
+	}
+	
+	private class SkinData {
+		String type;
+	}
+	
+	private class AnimCurveData {
+		long[] keyTimes;
+		float[] keyValues;
+	}
+	
+	private class AnimLayer {
+		String name;
+	}
+	
+	private class AnimNode {
+		String name;
+		Vector3f value;
+		AnimCurveData xCurve;
+		AnimCurveData yCurve;
+		AnimCurveData zCurve;
+		long layerId;
+	}
+	
+	private class PropertyLink {
+		long ref;
+		String propName;
+		
+		public PropertyLink(long id, String prop) {
+			this.ref = id;
+			this.propName = prop;
+		}
+	}
+	
+}