2
0
Эх сурвалжийг харах

First pass at copying the source files with history into the
new gradle-based structure.


git-svn-id: https://jmonkeyengine.googlecode.com/svn/branches/gradle-restructure@10964 75d07b2b-3a1a-0410-a2c5-0572b91ccdca

PSp..om 11 жил өмнө
commit
ed77d40c63
100 өөрчлөгдсөн 22911 нэмэгдсэн , 0 устгасан
  1. 640 0
      jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
  2. 20 0
      jme3-android/src/main/java/com/jme3/app/R.java
  3. 121 0
      jme3-android/src/main/java/com/jme3/asset/AndroidAssetManager.java
  4. 138 0
      jme3-android/src/main/java/com/jme3/asset/AndroidImageInfo.java
  5. 87 0
      jme3-android/src/main/java/com/jme3/asset/plugins/AndroidLocator.java
  6. 1054 0
      jme3-android/src/main/java/com/jme3/audio/android/AL.java
  7. 67 0
      jme3-android/src/main/java/com/jme3/audio/android/AndroidAudioData.java
  8. 24 0
      jme3-android/src/main/java/com/jme3/audio/android/AndroidAudioRenderer.java
  9. 523 0
      jme3-android/src/main/java/com/jme3/audio/android/AndroidMediaPlayerAudioRenderer.java
  10. 1423 0
      jme3-android/src/main/java/com/jme3/audio/android/AndroidOpenALSoftAudioRenderer.java
  11. 20 0
      jme3-android/src/main/java/com/jme3/audio/plugins/AndroidAudioLoader.java
  12. 348 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidGestureHandler.java
  13. 686 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidInput.java
  14. 273 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java
  15. 156 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyHandler.java
  16. 149 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java
  17. 795 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java
  18. 257 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler.java
  19. 152 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler14.java
  20. 121 0
      jme3-android/src/main/java/com/jme3/input/android/TouchEventPool.java
  21. 14 0
      jme3-android/src/main/java/com/jme3/renderer/android/Android22Workaround.java
  22. 26 0
      jme3-android/src/main/java/com/jme3/renderer/android/AndroidGLSurfaceView.java
  23. 2530 0
      jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java
  24. 129 0
      jme3-android/src/main/java/com/jme3/renderer/android/RendererUtil.java
  25. 571 0
      jme3-android/src/main/java/com/jme3/renderer/android/TextureUtil.java
  26. 518 0
      jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java
  27. 96 0
      jme3-android/src/main/java/com/jme3/system/android/AndroidTimer.java
  28. 226 0
      jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
  29. 467 0
      jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
  30. 20 0
      jme3-android/src/main/java/com/jme3/texture/plugins/AndroidImageLoader.java
  31. 110 0
      jme3-android/src/main/java/com/jme3/util/AndroidLogHandler.java
  32. 42 0
      jme3-android/src/main/java/com/jme3/util/AndroidScreenshots.java
  33. 76 0
      jme3-android/src/main/java/com/jme3/util/RingBuffer.java
  34. 29 0
      jme3-android/src/main/java/jme3test/android/AndroidManifest.xml
  35. 54 0
      jme3-android/src/main/java/jme3test/android/DemoAndroidHarness.java
  36. 72 0
      jme3-android/src/main/java/jme3test/android/DemoLaunchAdapter.java
  37. 38 0
      jme3-android/src/main/java/jme3test/android/DemoLaunchEntry.java
  38. 131 0
      jme3-android/src/main/java/jme3test/android/DemoMainActivity.java
  39. 46 0
      jme3-android/src/main/java/jme3test/android/R.java
  40. 40 0
      jme3-android/src/main/java/jme3test/android/SimpleSoundTest.java
  41. 150 0
      jme3-android/src/main/java/jme3test/android/SimpleTexturedTest.java
  42. 97 0
      jme3-android/src/main/java/jme3test/android/TestAmbient.java
  43. 95 0
      jme3-android/src/main/java/jme3test/android/TestBumpModel.java
  44. 102 0
      jme3-android/src/main/java/jme3test/android/TestMovingParticle.java
  45. 99 0
      jme3-android/src/main/java/jme3test/android/TestNormalMapping.java
  46. 70 0
      jme3-android/src/main/java/jme3test/android/TestSkyLoadingLagoon.java
  47. 68 0
      jme3-android/src/main/java/jme3test/android/TestSkyLoadingPrimitives.java
  48. 44 0
      jme3-android/src/main/java/jme3test/android/TestUnshadedModel.java
  49. 31 0
      jme3-android/src/main/resources/res/layout/about.xml
  50. 25 0
      jme3-android/src/main/resources/res/layout/tests.xml
  51. 12 0
      jme3-android/src/main/resources/res/menu/options.xml
  52. 6 0
      jme3-android/src/main/resources/res/values/strings.xml
  53. 909 0
      jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java
  54. 69 0
      jme3-blender/src/main/java/com/jme3/asset/GeneratedTextureKey.java
  55. 132 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java
  56. 636 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  57. 282 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java
  58. 105 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderModelLoader.java
  59. 29 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationData.java
  60. 276 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/ArmatureHelper.java
  61. 223 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneContext.java
  62. 243 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java
  63. 194 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/IpoHelper.java
  64. 147 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/cameras/CameraHelper.java
  65. 73 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/BoneConstraint.java
  66. 166 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java
  67. 478 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/ConstraintHelper.java
  68. 451 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SimulationNode.java
  69. 35 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SkeletonConstraint.java
  70. 27 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SpatialConstraint.java
  71. 146 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/VirtualTrack.java
  72. 135 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinition.java
  73. 77 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionDistLimit.java
  74. 124 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionFactory.java
  75. 117 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java
  76. 96 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLike.java
  77. 95 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLimit.java
  78. 28 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionNull.java
  79. 78 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLike.java
  80. 119 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLimit.java
  81. 64 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLike.java
  82. 86 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLimit.java
  83. 34 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/UnsupportedConstraintDefinition.java
  84. 146 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/BezierCurve.java
  85. 838 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/CurvesHelper.java
  86. 77 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderFileException.java
  87. 371 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderInputStream.java
  88. 203 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DnaBlockData.java
  89. 135 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DynamicArray.java
  90. 327 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Field.java
  91. 189 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/FileBlockHeader.java
  92. 189 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Pointer.java
  93. 315 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Structure.java
  94. 186 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java
  95. 118 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/lights/LightHelper.java
  96. 26 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/IAlphaMask.java
  97. 327 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java
  98. 371 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialHelper.java
  99. 88 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshContext.java
  100. 243 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java

+ 640 - 0
jme3-android/src/main/java/com/jme3/app/AndroidHarness.java

@@ -0,0 +1,640 @@
+package com.jme3.app;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.pm.ActivityInfo;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.*;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.audio.android.AndroidAudioRenderer;
+import com.jme3.input.JoyInput;
+import com.jme3.input.TouchInput;
+import com.jme3.input.android.AndroidSensorJoyInput;
+import com.jme3.input.controls.TouchListener;
+import com.jme3.input.controls.TouchTrigger;
+import com.jme3.input.event.TouchEvent;
+import com.jme3.renderer.android.AndroidGLSurfaceView;
+import com.jme3.system.AppSettings;
+import com.jme3.system.SystemListener;
+import com.jme3.system.android.AndroidConfigChooser.ConfigType;
+import com.jme3.system.android.JmeAndroidSystem;
+import com.jme3.system.android.OGLESContext;
+import com.jme3.util.AndroidLogHandler;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+/**
+ * <code>AndroidHarness</code> wraps a jme application object and runs it on
+ * Android
+ *
+ * @author Kirill
+ * @author larynx
+ */
+public class AndroidHarness extends Activity implements TouchListener, DialogInterface.OnClickListener, SystemListener {
+
+    protected final static Logger logger = Logger.getLogger(AndroidHarness.class.getName());
+    /**
+     * The application class to start
+     */
+    protected String appClass = "jme3test.android.Test";
+    /**
+     * The jme3 application object
+     */
+    protected Application app = null;
+
+    /**
+     * ConfigType.FASTEST is RGB565, GLSurfaceView default ConfigType.BEST is
+     * RGBA8888 or better if supported by the hardware
+     * @deprecated ConfigType has been deprecated.  AppSettings are now used
+     * to determine the desired configuration to match how LWJGL is implemented.
+     * Use eglBitsPerPixel, eglAlphaBits, eglDepthBits, eglStencilBits in MainActivity to
+     * override the default values
+     * (default values: RGB888, 0 alpha bits, 16 bit depth, 0 stencil bits)
+     */
+    @Deprecated
+    protected ConfigType eglConfigType = null;
+
+    /**
+     * Sets the desired RGB size for the surfaceview.  16 = RGB565, 24 = RGB888.
+     * (default = 24)
+     */
+    protected int eglBitsPerPixel = 24;
+
+    /**
+     * Sets the desired number of Alpha bits for the surfaceview.  This affects
+     * how the surfaceview is able to display Android views that are located
+     * under the surfaceview jME uses to render the scenegraph.
+     * 0 = Opaque surfaceview background (fastest)
+     * 1->7 = Transparent surfaceview background
+     * 8 or higher = Translucent surfaceview background
+     * (default = 0)
+     */
+    protected int eglAlphaBits = 0;
+
+    /**
+     * The number of depth bits specifies the precision of the depth buffer.
+     * (default = 16)
+     */
+    protected int eglDepthBits = 16;
+
+    /**
+     * Sets the number of samples to use for multisampling.</br>
+     * Leave 0 (default) to disable multisampling.</br>
+     * Set to 2 or 4 to enable multisampling.
+     */
+    protected int eglSamples = 0;
+
+    /**
+     * Set the number of stencil bits.
+     * (default = 0)
+     */
+    protected int eglStencilBits = 0;
+
+    /**
+     * If true all valid and not valid egl configs are logged
+     * @deprecated this has no use
+     */
+    @Deprecated
+    protected boolean eglConfigVerboseLogging = false;
+
+    /**
+     * set to 2, 4 to enable multisampling.
+     * @deprecated Use eglSamples
+     */
+    @Deprecated
+    protected int antiAliasingSamples = 0;
+
+    /**
+     * Sets the type of Audio Renderer to be used.
+     * <p>
+     * Android MediaPlayer / SoundPool is the default and can be used on all
+     * supported Android platform versions (2.2+)<br>
+     * OpenAL Soft uses an OpenSL backend and is only supported on Android
+     * versions 2.3+.
+     * <p>
+     * Only use ANDROID_ static strings found in AppSettings
+     *
+     */
+    protected String audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
+
+    /**
+     * If true Android Sensors are used as simulated Joysticks. Users can use the
+     * Android sensor feedback through the RawInputListener or by registering
+     * JoyAxisTriggers.
+     */
+    protected boolean joystickEventsEnabled = false;
+    /**
+     * If true MouseEvents are generated from TouchEvents
+     */
+    protected boolean mouseEventsEnabled = true;
+    /**
+     * Flip X axis
+     */
+    protected boolean mouseEventsInvertX = false;
+    /**
+     * Flip Y axis
+     */
+    protected boolean mouseEventsInvertY = false;
+    /**
+     * if true finish this activity when the jme app is stopped
+     */
+    protected boolean finishOnAppStop = true;
+    /**
+     * set to false if you don't want the harness to handle the exit hook
+     */
+    protected boolean handleExitHook = true;
+    /**
+     * Title of the exit dialog, default is "Do you want to exit?"
+     */
+    protected String exitDialogTitle = "Do you want to exit?";
+    /**
+     * Message of the exit dialog, default is "Use your home key to bring this
+     * app into the background or exit to terminate it."
+     */
+    protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it.";
+    /**
+     * Set the screen window mode. If screenFullSize is true, then the
+     * notification bar and title bar are removed and the screen covers the
+     * entire display. If screenFullSize is false, then the notification bar
+     * remains visible if screenShowTitle is true while screenFullScreen is
+     * false, then the title bar is also displayed under the notification bar.
+     */
+    protected boolean screenFullScreen = true;
+    /**
+     * if screenShowTitle is true while screenFullScreen is false, then the
+     * title bar is also displayed under the notification bar
+     */
+    protected boolean screenShowTitle = true;
+    /**
+     * Splash Screen picture Resource ID. If a Splash Screen is desired, set
+     * splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If
+     * splashPicID = 0, then no splash screen will be displayed.
+     */
+    protected int splashPicID = 0;
+    /**
+     * Set the screen orientation, default is SENSOR
+     * ActivityInfo.SCREEN_ORIENTATION_* constants package
+     * android.content.pm.ActivityInfo
+     *
+     * SCREEN_ORIENTATION_UNSPECIFIED SCREEN_ORIENTATION_LANDSCAPE
+     * SCREEN_ORIENTATION_PORTRAIT SCREEN_ORIENTATION_USER
+     * SCREEN_ORIENTATION_BEHIND SCREEN_ORIENTATION_SENSOR (default)
+     * SCREEN_ORIENTATION_NOSENSOR
+     */
+    protected int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR;
+    protected OGLESContext ctx;
+    protected AndroidGLSurfaceView view = null;
+    protected boolean isGLThreadPaused = true;
+    protected ImageView splashImageView = null;
+    protected FrameLayout frameLayout = null;
+    final private String ESCAPE_EVENT = "TouchEscape";
+    private boolean firstDrawFrame = true;
+    private boolean inConfigChange = false;
+
+    private class DataObject {
+        protected Application app = null;
+    }
+
+    @Override
+    public Object onRetainNonConfigurationInstance() {
+        logger.log(Level.FINE, "onRetainNonConfigurationInstance");
+        final DataObject data = new DataObject();
+        data.app = this.app;
+        inConfigChange = true;
+
+        return data;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        initializeLogHandler();
+
+        logger.fine("onCreate");
+        super.onCreate(savedInstanceState);
+
+        JmeAndroidSystem.setActivity(this);
+        if (screenFullScreen) {
+            requestWindowFeature(Window.FEATURE_NO_TITLE);
+            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                                 WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        } else {
+            if (!screenShowTitle) {
+                requestWindowFeature(Window.FEATURE_NO_TITLE);
+            }
+        }
+
+        setRequestedOrientation(screenOrientation);
+
+        final DataObject data = (DataObject) getLastNonConfigurationInstance();
+        if (data != null) {
+            logger.log(Level.FINE, "Using Retained App");
+            this.app = data.app;
+        } else {
+            // Discover the screen reolution
+            //TODO try to find a better way to get a hand on the resolution
+            WindowManager wind = this.getWindowManager();
+            Display disp = wind.getDefaultDisplay();
+            Log.d("AndroidHarness", "Resolution from Window, width:" + disp.getWidth() + ", height: " + disp.getHeight());
+
+            // Create Settings
+            logger.log(Level.FINE, "Creating settings");
+            AppSettings settings = new AppSettings(true);
+            settings.setEmulateMouse(mouseEventsEnabled);
+            settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY);
+            settings.setUseJoysticks(joystickEventsEnabled);
+            if (eglConfigType == null) {
+                logger.log(Level.FINE, "using new appsettings for eglConfig");
+                settings.setBitsPerPixel(eglBitsPerPixel);
+                settings.setAlphaBits(eglAlphaBits);
+                settings.setDepthBits(eglDepthBits);
+                settings.setSamples(eglSamples);
+                settings.setStencilBits(eglStencilBits);
+            } else {
+                logger.log(Level.FINE, "using old eglConfigType {0} for eglConfig", eglConfigType);
+                switch (eglConfigType) {
+                    case BEST:
+                        settings.setBitsPerPixel(24);
+                        settings.setAlphaBits(0);
+                        settings.setDepthBits(16);
+                        settings.setStencilBits(0);
+                        break;
+                    case FASTEST:
+                    case LEGACY:
+                        settings.setBitsPerPixel(16);
+                        settings.setAlphaBits(0);
+                        settings.setDepthBits(16);
+                        settings.setStencilBits(0);
+                        break;
+                    case BEST_TRANSLUCENT:
+                        settings.setBitsPerPixel(24);
+                        settings.setAlphaBits(8);
+                        settings.setDepthBits(16);
+                        settings.setStencilBits(0);
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Invalid eglConfigType");
+                }
+                settings.setSamples(antiAliasingSamples);
+            }
+            settings.setResolution(disp.getWidth(), disp.getHeight());
+            settings.setAudioRenderer(audioRendererType);
+
+
+            // Create application instance
+            try {
+                if (app == null) {
+                    @SuppressWarnings("unchecked")
+                    Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass);
+                    app = clazz.newInstance();
+                }
+
+                app.setSettings(settings);
+                app.start();
+            } catch (Exception ex) {
+                handleError("Class " + appClass + " init failed", ex);
+                setContentView(new TextView(this));
+            }
+        }
+
+        ctx = (OGLESContext) app.getContext();
+        view = ctx.createView();
+        // AndroidHarness wraps the app as a SystemListener.
+        ctx.setSystemListener(this);
+        layoutDisplay();
+    }
+
+    @Override
+    protected void onRestart() {
+        logger.fine("onRestart");
+        super.onRestart();
+        if (app != null) {
+            app.restart();
+        }
+    }
+
+    @Override
+    protected void onStart() {
+        logger.fine("onStart");
+        super.onStart();
+    }
+
+    @Override
+    protected void onResume() {
+        logger.fine("onResume");
+        super.onResume();
+
+        gainFocus();
+    }
+
+    @Override
+    protected void onPause() {
+        logger.fine("onPause");
+        loseFocus();
+
+        super.onPause();
+    }
+
+    @Override
+    protected void onStop() {
+        logger.fine("onStop");
+        super.onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        logger.fine("onDestroy");
+        final DataObject data = (DataObject) getLastNonConfigurationInstance();
+        if (data != null || inConfigChange) {
+            logger.fine("In Config Change, not stopping app.");
+        } else {
+            if (app != null) {
+                app.stop(!isGLThreadPaused);
+            }
+        }
+        setContentView(new TextView(this));
+        JmeAndroidSystem.setActivity(null);
+        ctx = null;
+        app = null;
+        view = null;
+
+        super.onDestroy();
+    }
+
+    public Application getJmeApplication() {
+        return app;
+    }
+
+    /**
+     * Called when an error has occurred. By default, will show an error message
+     * to the user and print the exception/error to the log.
+     */
+    @Override
+    public void handleError(final String errorMsg, final Throwable t) {
+        String stackTrace = "";
+        String title = "Error";
+
+        if (t != null) {
+            // Convert exception to string
+            StringWriter sw = new StringWriter(100);
+            t.printStackTrace(new PrintWriter(sw));
+            stackTrace = sw.toString();
+            title = t.toString();
+        }
+
+        final String finalTitle = title;
+        final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception")
+                + "\n" + stackTrace;
+
+        logger.log(Level.SEVERE, finalMsg);
+
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon)
+                        .setTitle(finalTitle).setPositiveButton("Kill", AndroidHarness.this).setMessage(finalMsg).create();
+                dialog.show();
+            }
+        });
+    }
+
+    /**
+     * Called by the android alert dialog, terminate the activity and OpenGL
+     * rendering
+     *
+     * @param dialog
+     * @param whichButton
+     */
+    public void onClick(DialogInterface dialog, int whichButton) {
+        if (whichButton != -2) {
+            if (app != null) {
+                app.stop(true);
+            }
+            app = null;
+            this.finish();
+        }
+    }
+
+    /**
+     * Gets called by the InputManager on all touch/drag/scale events
+     */
+    @Override
+    public void onTouch(String name, TouchEvent evt, float tpf) {
+        if (name.equals(ESCAPE_EVENT)) {
+            switch (evt.getType()) {
+                case KEY_UP:
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon)
+                                    .setTitle(exitDialogTitle).setPositiveButton("Yes", AndroidHarness.this).setNegativeButton("No", AndroidHarness.this).setMessage(exitDialogMessage).create();
+                            dialog.show();
+                        }
+                    });
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    public void layoutDisplay() {
+        logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID);
+        if (view == null) {
+            logger.log(Level.FINE, "view is null!");
+        }
+        if (splashPicID != 0) {
+            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
+                    LayoutParams.FILL_PARENT,
+                    LayoutParams.FILL_PARENT,
+                    Gravity.CENTER);
+
+            frameLayout = new FrameLayout(this);
+            splashImageView = new ImageView(this);
+
+            Drawable drawable = this.getResources().getDrawable(splashPicID);
+            if (drawable instanceof NinePatchDrawable) {
+                splashImageView.setBackgroundDrawable(drawable);
+            } else {
+                splashImageView.setImageResource(splashPicID);
+            }
+
+            if (view.getParent() != null) {
+                ((ViewGroup) view.getParent()).removeView(view);
+            }
+            frameLayout.addView(view);
+
+            if (splashImageView.getParent() != null) {
+                ((ViewGroup) splashImageView.getParent()).removeView(splashImageView);
+            }
+            frameLayout.addView(splashImageView, lp);
+
+            setContentView(frameLayout);
+            logger.log(Level.FINE, "Splash Screen Created");
+        } else {
+            logger.log(Level.FINE, "Splash Screen Skipped.");
+            setContentView(view);
+        }
+    }
+
+    public void removeSplashScreen() {
+        logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID);
+        if (splashPicID != 0) {
+            if (frameLayout != null) {
+                if (splashImageView != null) {
+                    this.runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            splashImageView.setVisibility(View.INVISIBLE);
+                            frameLayout.removeView(splashImageView);
+                        }
+                    });
+                } else {
+                    logger.log(Level.FINE, "splashImageView is null");
+                }
+            } else {
+                logger.log(Level.FINE, "frameLayout is null");
+            }
+        }
+    }
+
+    /**
+     * Removes the standard Android log handler due to an issue with not logging
+     * entries lower than INFO level and adds a handler that produces
+     * JME formatted log messages.
+     */
+    protected void initializeLogHandler() {
+        Logger log = LogManager.getLogManager().getLogger("");
+        for (Handler handler : log.getHandlers()) {
+            if (log.getLevel() != null && log.getLevel().intValue() <= Level.FINE.intValue()) {
+                Log.v("AndroidHarness", "Removing Handler class: " + handler.getClass().getName());
+            }
+            log.removeHandler(handler);
+        }
+        Handler handler = new AndroidLogHandler();
+        log.addHandler(handler);
+        handler.setLevel(Level.ALL);
+    }
+
+    public void initialize() {
+        app.initialize();
+        if (handleExitHook) {
+            // remove existing mapping from SimpleApplication that stops the app
+            // when the esc key is pressed (esc key = android back key) so that
+            // AndroidHarness can produce the exit app dialog box.
+            if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) {
+                app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT);
+            }
+            
+            app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
+            app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT});
+        }
+    }
+
+    public void reshape(int width, int height) {
+        app.reshape(width, height);
+    }
+
+    public void update() {
+        app.update();
+        // call to remove the splash screen, if present.
+        // call after app.update() to make sure no gap between
+        // splash screen going away and app display being shown.
+        if (firstDrawFrame) {
+            removeSplashScreen();
+            firstDrawFrame = false;
+        }
+    }
+
+    public void requestClose(boolean esc) {
+        app.requestClose(esc);
+    }
+
+    public void destroy() {
+        if (app != null) {
+            app.destroy();
+        }
+        if (finishOnAppStop) {
+            finish();
+        }
+    }
+
+    public void gainFocus() {
+        logger.fine("gainFocus");
+        if (view != null) {
+            view.onResume();
+        }
+
+        if (app != null) {
+            //resume the audio
+            AudioRenderer result = app.getAudioRenderer();
+            if (result != null) {
+                if (result instanceof AndroidAudioRenderer) {
+                    AndroidAudioRenderer renderer = (AndroidAudioRenderer) result;
+                    renderer.resumeAll();
+                }
+            }
+            //resume the sensors (aka joysticks)
+            if (app.getContext() != null) {
+                JoyInput joyInput = app.getContext().getJoyInput();
+                if (joyInput != null) {
+                    if (joyInput instanceof AndroidSensorJoyInput) {
+                        AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput;
+                        androidJoyInput.resumeSensors();
+                    }
+                }
+            }
+        }
+
+        isGLThreadPaused = false;
+
+        if (app != null) {
+            app.gainFocus();
+        }
+    }
+
+    public void loseFocus() {
+        logger.fine("loseFocus");
+        if (app != null) {
+            app.loseFocus();
+        }
+
+        if (view != null) {
+            view.onPause();
+        }
+
+        if (app != null) {
+            //pause the audio
+            AudioRenderer result = app.getAudioRenderer();
+            if (result != null) {
+                logger.log(Level.FINE, "pause: {0}", result.getClass().getSimpleName());
+                if (result instanceof AndroidAudioRenderer) {
+                    AndroidAudioRenderer renderer = (AndroidAudioRenderer) result;
+                    renderer.pauseAll();
+                }
+            }
+            //pause the sensors (aka joysticks)
+            if (app.getContext() != null) {
+                JoyInput joyInput = app.getContext().getJoyInput();
+                if (joyInput != null) {
+                    if (joyInput instanceof AndroidSensorJoyInput) {
+                        AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput;
+                        androidJoyInput.pauseSensors();
+                    }
+                }
+            }
+        }
+        isGLThreadPaused = true;
+    }
+}

+ 20 - 0
jme3-android/src/main/java/com/jme3/app/R.java

@@ -0,0 +1,20 @@
+/* AUTO-GENERATED FILE.  DO NOT MODIFY.
+ *
+ * This class was automatically generated by the
+ * aapt tool from the resource data it found.  It
+ * should not be modified by hand.
+ */
+
+package com.jme3.app;
+
+public final class R {
+    public static final class attr {
+    }
+    public static final class layout {
+        public static final int main=0x7f020000;
+    }
+    public static final class string {
+        public static final int app_name=0x7f030000;
+        public static final int jme3_appclass=0x7f030001;
+    }
+}

+ 121 - 0
jme3-android/src/main/java/com/jme3/asset/AndroidAssetManager.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2009-2012 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.asset;
+
+import com.jme3.asset.plugins.AndroidLocator;
+import com.jme3.asset.plugins.ClasspathLocator;
+import com.jme3.audio.android.AndroidAudioRenderer;
+import com.jme3.audio.plugins.AndroidAudioLoader;
+import com.jme3.audio.plugins.WAVLoader;
+import com.jme3.system.AppSettings;
+import com.jme3.system.android.JmeAndroidSystem;
+import com.jme3.texture.plugins.AndroidImageLoader;
+import java.net.URL;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <code>AndroidAssetManager</code> is an implementation of DesktopAssetManager for Android
+ *
+ * @author larynx
+ */
+public class AndroidAssetManager extends DesktopAssetManager {
+
+    private static final Logger logger = Logger.getLogger(AndroidAssetManager.class.getName());
+
+    public AndroidAssetManager() {
+        this(null);
+    }
+
+    @Deprecated
+    public AndroidAssetManager(boolean loadDefaults) {
+        //this(Thread.currentThread().getContextClassLoader().getResource("com/jme3/asset/Android.cfg"));
+        this(null);
+    }
+
+    private void registerLoaderSafe(String loaderClass, String ... extensions) {
+        try {
+            Class<? extends AssetLoader> loader = (Class<? extends AssetLoader>) Class.forName(loaderClass);
+            registerLoader(loader, extensions);
+        } catch (Exception e){
+            logger.log(Level.WARNING, "Failed to load AssetLoader", e);
+        }
+    }
+
+    /**
+     * AndroidAssetManager constructor
+     * If URL == null then a default list of locators and loaders for android is set
+     * @param configFile
+     */
+    public AndroidAssetManager(URL configFile) {
+        System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver");
+
+        // Set Default Android config
+        registerLocator("", AndroidLocator.class);
+        registerLocator("", ClasspathLocator.class);
+
+        registerLoader(AndroidImageLoader.class, "jpg", "bmp", "gif", "png", "jpeg");
+        if (JmeAndroidSystem.getAudioRendererType().equals(AppSettings.ANDROID_MEDIAPLAYER)) {
+            registerLoader(AndroidAudioLoader.class, "ogg", "mp3", "wav");
+        } else if (JmeAndroidSystem.getAudioRendererType().equals(AppSettings.ANDROID_OPENAL_SOFT)) {
+            registerLoader(WAVLoader.class, "wav");
+            // TODO jogg is not in core, need to add some other way to get around compile errors, or not.
+//            registerLoader(com.jme3.audio.plugins.OGGLoader.class, "ogg");
+            registerLoaderSafe("com.jme3.audio.plugins.OGGLoader", "ogg");
+        } else {
+            throw new IllegalStateException("No Audio Renderer Type defined!");
+        }
+
+        registerLoader(com.jme3.material.plugins.J3MLoader.class, "j3m");
+        registerLoader(com.jme3.material.plugins.J3MLoader.class, "j3md");
+        registerLoader(com.jme3.material.plugins.ShaderNodeDefinitionLoader.class, "j3sn");
+        registerLoader(com.jme3.shader.plugins.GLSLLoader.class, "vert", "frag", "glsl", "glsllib");
+        registerLoader(com.jme3.export.binary.BinaryImporter.class, "j3o");
+        registerLoader(com.jme3.font.plugins.BitmapFontLoader.class, "fnt");
+
+        // Less common loaders (especially on Android)
+        registerLoaderSafe("com.jme3.texture.plugins.DDSLoader", "dds");
+        registerLoaderSafe("com.jme3.texture.plugins.PFMLoader", "pfm");
+        registerLoaderSafe("com.jme3.texture.plugins.HDRLoader", "hdr");
+        registerLoaderSafe("com.jme3.texture.plugins.TGALoader", "tga");
+        registerLoaderSafe("com.jme3.scene.plugins.OBJLoader", "obj");
+        registerLoaderSafe("com.jme3.scene.plugins.MTLLoader", "mtl");
+        registerLoaderSafe("com.jme3.scene.plugins.ogre.MeshLoader", "mesh.xml");
+        registerLoaderSafe("com.jme3.scene.plugins.ogre.SkeletonLoader", "skeleton.xml");
+        registerLoaderSafe("com.jme3.scene.plugins.ogre.MaterialLoader", "material");
+        registerLoaderSafe("com.jme3.scene.plugins.ogre.SceneLoader", "scene");
+
+
+        logger.fine("AndroidAssetManager created.");
+    }
+
+}

+ 138 - 0
jme3-android/src/main/java/com/jme3/asset/AndroidImageInfo.java

@@ -0,0 +1,138 @@
+package com.jme3.asset;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import com.jme3.math.ColorRGBA;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.image.ImageRaster;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+  * <code>AndroidImageInfo</code> is set in a jME3 image via the {@link Image#setEfficentData(java.lang.Object) }
+  * method to retrieve a {@link Bitmap} when it is needed by the renderer. 
+  * User code may extend <code>AndroidImageInfo</code> and provide their own implementation of the 
+  * {@link AndroidImageInfo#loadBitmap()} method to acquire a bitmap by their own means.
+  *
+  * @author Kirill Vainer
+  */
+public class AndroidImageInfo extends ImageRaster {
+    
+    private static final Logger logger = Logger.getLogger(AndroidImageInfo.class.getName());
+    
+    protected AssetInfo assetInfo;
+    protected Bitmap bitmap;
+    protected Format format;
+
+    public AndroidImageInfo(AssetInfo assetInfo) {
+        this.assetInfo = assetInfo;
+    }
+    
+    public Bitmap getBitmap(){
+        if (bitmap == null || bitmap.isRecycled()){
+            try {
+                loadBitmap();
+            } catch (IOException ex) {
+                // If called first inside AssetManager, the error will propagate
+                // correctly. Assuming that if the first calls succeeds
+                // then subsequent calls will as well.
+                throw new AssetLoadException("Failed to load image " + assetInfo.getKey(), ex);
+            }
+        }
+        return bitmap;
+    }
+    
+    public void notifyBitmapUploaded() {
+        // Default function is to recycle the bitmap.
+        if (bitmap != null && !bitmap.isRecycled()) {
+            bitmap.recycle();
+            bitmap = null;
+            logger.log(Level.FINE, "Bitmap was deleted. ");
+        }
+    }
+    
+    public Format getFormat(){
+        return format;
+    }
+    
+    @Override
+    public int getWidth() {
+        return getBitmap().getWidth();
+    }
+
+    @Override
+    public int getHeight() {
+        return getBitmap().getHeight();
+    }
+    
+    @Override
+    public void setPixel(int x, int y, ColorRGBA color) {
+        getBitmap().setPixel(x, y, color.asIntARGB());
+    }
+
+    @Override
+    public ColorRGBA getPixel(int x, int y, ColorRGBA store) {
+        if (store == null) {
+            store = new ColorRGBA();
+        }
+        store.fromIntARGB(getBitmap().getPixel(x, y));
+        return store;
+    }
+    
+    /**
+     * Loads the bitmap directly from the asset info, possibly updating
+     * or creating the image object.
+     */
+    protected void loadBitmap() throws IOException{
+        InputStream in = null;
+        try {
+            in = assetInfo.openStream();
+            bitmap = BitmapFactory.decodeStream(in);
+            if (bitmap == null) {
+                throw new IOException("Failed to load image: " + assetInfo.getKey().getName());
+            }
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+
+        switch (bitmap.getConfig()) {
+            case ALPHA_8:
+                format = Image.Format.Alpha8;
+                break;
+            case ARGB_4444:
+                format = Image.Format.ARGB4444;
+                break;
+            case ARGB_8888:
+                format = Image.Format.RGBA8;
+                break;
+            case RGB_565:
+                format = Image.Format.RGB565;
+                break;
+            default:
+                // This should still work as long
+                // as renderer doesn't check format
+                // but just loads bitmap directly.
+                format = null;
+        }
+
+        TextureKey texKey = (TextureKey) assetInfo.getKey();
+        if (texKey.isFlipY()) {
+            // Flip the image, then delete the old one.
+            Matrix flipMat = new Matrix();
+            flipMat.preScale(1.0f, -1.0f);
+            Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), flipMat, false);
+            bitmap.recycle();
+            bitmap = newBitmap;
+
+            if (bitmap == null) {
+                throw new IOException("Failed to flip image: " + texKey);
+            }
+        }  
+    }
+}

+ 87 - 0
jme3-android/src/main/java/com/jme3/asset/plugins/AndroidLocator.java

@@ -0,0 +1,87 @@
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.*;
+import com.jme3.system.android.JmeAndroidSystem;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+
+public class AndroidLocator implements AssetLocator {
+
+    private static final Logger logger = Logger.getLogger(AndroidLocator.class.getName());
+    
+    private android.content.res.AssetManager androidManager;
+    private String rootPath = "";
+
+    private class AndroidAssetInfo extends AssetInfo {
+
+        private InputStream in;
+        private final String assetPath;
+
+        public AndroidAssetInfo(com.jme3.asset.AssetManager assetManager, AssetKey<?> key, String assetPath, InputStream in) {
+            super(assetManager, key);
+            this.assetPath = assetPath;
+            this.in = in;
+        }
+        
+        @Override
+        public InputStream openStream() {
+            if (in != null){
+                // Reuse the already existing stream (only once)
+                InputStream in2 = in;
+                in = null;
+                return in2;
+            }else{
+                // Create a new stream for subsequent invocations.
+                try {
+                    return androidManager.open(assetPath);
+                } catch (IOException ex) {
+                    throw new AssetLoadException("Failed to open asset " + assetPath, ex);
+                }
+            }
+        }
+    }
+
+    private AndroidAssetInfo create(AssetManager assetManager, AssetKey key, String assetPath) throws IOException {
+        try {
+            InputStream in = androidManager.open(assetPath);
+            if (in == null){
+                return null;
+            }else{
+                return new AndroidAssetInfo(assetManager, key, assetPath, in);
+            }
+        } catch (IOException ex) {
+            // XXX: Prefer to show warning here?
+            // Should only surpress exceptions for "file missing" type errors.
+            return null;
+        }
+    }
+    
+    public AndroidLocator() {
+        androidManager = JmeAndroidSystem.getActivity().getAssets();
+    }
+
+    public void setRootPath(String rootPath) {
+        this.rootPath = rootPath;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public AssetInfo locate(com.jme3.asset.AssetManager manager, AssetKey key) {
+        String assetPath = rootPath + key.getName();
+        // Fix path issues
+        if (assetPath.startsWith("/")) {
+            // Remove leading /
+            assetPath = assetPath.substring(1);
+        }
+        assetPath = assetPath.replace("//", "/");
+        try {
+            return create(manager, key, assetPath);
+        } catch (IOException ex) {
+            // This is different handling than URL locator
+            // since classpath locating would return null at the getResource() 
+            // call, otherwise there's a more critical error...
+            throw new AssetLoadException("Failed to open asset " + assetPath, ex);
+        }
+    }
+}

+ 1054 - 0
jme3-android/src/main/java/com/jme3/audio/android/AL.java

@@ -0,0 +1,1054 @@
+package com.jme3.audio.android;
+
+/**
+ *
+ * @author iwgeric
+ */
+public class AL {
+
+
+
+    /* ********** */
+    /* FROM ALC.h */
+    /* ********** */
+
+//    typedef struct ALCdevice_struct ALCdevice;
+//    typedef struct ALCcontext_struct ALCcontext;
+
+
+    /**
+     * No error
+     */
+    static final int ALC_NO_ERROR = 0;
+
+    /**
+     * No device
+     */
+    static final int ALC_INVALID_DEVICE = 0xA001;
+
+    /**
+     * invalid context ID
+     */
+    static final int ALC_INVALID_CONTEXT = 0xA002;
+
+    /**
+     * bad enum
+     */
+    static final int ALC_INVALID_ENUM = 0xA003;
+
+    /**
+     * bad value
+     */
+    static final int ALC_INVALID_VALUE = 0xA004;
+
+    /**
+     * Out of memory.
+     */
+    static final int ALC_OUT_OF_MEMORY = 0xA005;
+
+
+    /**
+     * The Specifier string for default device
+     */
+    static final int ALC_DEFAULT_DEVICE_SPECIFIER = 0x1004;
+    static final int ALC_DEVICE_SPECIFIER = 0x1005;
+    static final int ALC_EXTENSIONS = 0x1006;
+
+    static final int ALC_MAJOR_VERSION = 0x1000;
+    static final int ALC_MINOR_VERSION = 0x1001;
+
+    static final int ALC_ATTRIBUTES_SIZE = 0x1002;
+    static final int ALC_ALL_ATTRIBUTES = 0x1003;
+
+
+    /**
+     * Capture extension
+     */
+    static final int ALC_EXT_CAPTURE = 1;
+    static final int ALC_CAPTURE_DEVICE_SPECIFIER = 0x310;
+    static final int ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER = 0x311;
+    static final int ALC_CAPTURE_SAMPLES = 0x312;
+
+
+    /**
+     * ALC_ENUMERATE_ALL_EXT enums
+     */
+    static final int ALC_ENUMERATE_ALL_EXT = 1;
+    static final int ALC_DEFAULT_ALL_DEVICES_SPECIFIER = 0x1012;
+    static final int ALC_ALL_DEVICES_SPECIFIER = 0x1013;
+
+
+    /* ********** */
+    /* FROM AL.h */
+    /* ********** */
+
+/** Boolean False. */
+    static final int AL_FALSE = 0;
+
+/** Boolean True. */
+    static final int AL_TRUE = 1;
+
+/* "no distance model" or "no buffer" */
+    static final int AL_NONE = 0;
+
+/** Indicate Source has relative coordinates. */
+    static final int AL_SOURCE_RELATIVE = 0x202;
+
+
+
+/**
+ * Directional source, inner cone angle, in degrees.
+ * Range:    [0-360]
+ * Default:  360
+ */
+    static final int AL_CONE_INNER_ANGLE = 0x1001;
+
+/**
+ * Directional source, outer cone angle, in degrees.
+ * Range:    [0-360]
+ * Default:  360
+ */
+    static final int AL_CONE_OUTER_ANGLE = 0x1002;
+
+/**
+ * Specify the pitch to be applied at source.
+ * Range:   [0.5-2.0]
+ * Default: 1.0
+ */
+    static final int AL_PITCH = 0x1003;
+
+/**
+ * Specify the current location in three dimensional space.
+ * OpenAL, like OpenGL, uses a right handed coordinate system,
+ *  where in a frontal default view X (thumb) points right,
+ *  Y points up (index finger), and Z points towards the
+ *  viewer/camera (middle finger).
+ * To switch from a left handed coordinate system, flip the
+ *  sign on the Z coordinate.
+ * Listener position is always in the world coordinate system.
+ */
+    static final int AL_POSITION = 0x1004;
+
+/** Specify the current direction. */
+    static final int AL_DIRECTION = 0x1005;
+
+/** Specify the current velocity in three dimensional space. */
+    static final int AL_VELOCITY = 0x1006;
+
+/**
+ * Indicate whether source is looping.
+ * Type: ALboolean?
+ * Range:   [AL_TRUE, AL_FALSE]
+ * Default: FALSE.
+ */
+    static final int AL_LOOPING = 0x1007;
+
+/**
+ * Indicate the buffer to provide sound samples.
+ * Type: ALuint.
+ * Range: any valid Buffer id.
+ */
+    static final int AL_BUFFER = 0x1009;
+
+/**
+ * Indicate the gain (volume amplification) applied.
+ * Type:   ALfloat.
+ * Range:  ]0.0-  ]
+ * A value of 1.0 means un-attenuated/unchanged.
+ * Each division by 2 equals an attenuation of -6dB.
+ * Each multiplicaton with 2 equals an amplification of +6dB.
+ * A value of 0.0 is meaningless with respect to a logarithmic
+ *  scale; it is interpreted as zero volume - the channel
+ *  is effectively disabled.
+ */
+    static final int AL_GAIN = 0x100A;
+
+/*
+ * Indicate minimum source attenuation
+ * Type: ALfloat
+ * Range:  [0.0 - 1.0]
+ *
+ * Logarthmic
+ */
+    static final int AL_MIN_GAIN = 0x100D;
+
+/**
+ * Indicate maximum source attenuation
+ * Type: ALfloat
+ * Range:  [0.0 - 1.0]
+ *
+ * Logarthmic
+ */
+    static final int AL_MAX_GAIN = 0x100E;
+
+/**
+ * Indicate listener orientation.
+ *
+ * at/up
+ */
+    static final int AL_ORIENTATION = 0x100F;
+
+/**
+ * Source state information.
+ */
+    static final int AL_SOURCE_STATE = 0x1010;
+    static final int AL_INITIAL = 0x1011;
+    static final int AL_PLAYING = 0x1012;
+    static final int AL_PAUSED = 0x1013;
+    static final int AL_STOPPED = 0x1014;
+
+/**
+ * Buffer Queue params
+ */
+    static final int AL_BUFFERS_QUEUED = 0x1015;
+    static final int AL_BUFFERS_PROCESSED = 0x1016;
+
+/**
+ * Source buffer position information
+ */
+    static final int AL_SEC_OFFSET = 0x1024;
+    static final int AL_SAMPLE_OFFSET = 0x1025;
+    static final int AL_BYTE_OFFSET = 0x1026;
+
+/*
+ * Source type (Static, Streaming or undetermined)
+ * Source is Static if a Buffer has been attached using AL_BUFFER
+ * Source is Streaming if one or more Buffers have been attached using alSourceQueueBuffers
+ * Source is undetermined when it has the NULL buffer attached
+ */
+    static final int AL_SOURCE_TYPE = 0x1027;
+    static final int AL_STATIC = 0x1028;
+    static final int AL_STREAMING = 0x1029;
+    static final int AL_UNDETERMINED = 0x1030;
+
+/** Sound samples: format specifier. */
+    static final int AL_FORMAT_MONO8 = 0x1100;
+    static final int AL_FORMAT_MONO16 = 0x1101;
+    static final int AL_FORMAT_STEREO8 = 0x1102;
+    static final int AL_FORMAT_STEREO16 = 0x1103;
+
+/**
+ * source specific reference distance
+ * Type: ALfloat
+ * Range:  0.0 - +inf
+ *
+ * At 0.0, no distance attenuation occurs.  Default is
+ * 1.0.
+ */
+    static final int AL_REFERENCE_DISTANCE = 0x1020;
+
+/**
+ * source specific rolloff factor
+ * Type: ALfloat
+ * Range:  0.0 - +inf
+ *
+ */
+    static final int AL_ROLLOFF_FACTOR = 0x1021;
+
+/**
+ * Directional source, outer cone gain.
+ *
+ * Default:  0.0
+ * Range:    [0.0 - 1.0]
+ * Logarithmic
+ */
+    static final int AL_CONE_OUTER_GAIN = 0x1022;
+
+/**
+ * Indicate distance above which sources are not
+ * attenuated using the inverse clamped distance model.
+ *
+ * Default: +inf
+ * Type: ALfloat
+ * Range:  0.0 - +inf
+ */
+    static final int AL_MAX_DISTANCE = 0x1023;
+
+/**
+ * Sound samples: frequency, in units of Hertz [Hz].
+ * This is the number of samples per second. Half of the
+ *  sample frequency marks the maximum significant
+ *  frequency component.
+ */
+    static final int AL_FREQUENCY = 0x2001;
+    static final int AL_BITS = 0x2002;
+    static final int AL_CHANNELS = 0x2003;
+    static final int AL_SIZE = 0x2004;
+
+/**
+ * Buffer state.
+ *
+ * Not supported for public use (yet).
+ */
+    static final int AL_UNUSED = 0x2010;
+    static final int AL_PENDING = 0x2011;
+    static final int AL_PROCESSED = 0x2012;
+
+
+/** Errors: No Error. */
+    static final int AL_NO_ERROR = 0;
+
+/**
+ * Invalid Name paramater passed to AL call.
+ */
+    static final int AL_INVALID_NAME = 0xA001;
+
+/**
+ * Invalid parameter passed to AL call.
+ */
+    static final int AL_INVALID_ENUM = 0xA002;
+
+/**
+ * Invalid enum parameter value.
+ */
+    static final int AL_INVALID_VALUE = 0xA003;
+
+/**
+ * Illegal call.
+ */
+    static final int AL_INVALID_OPERATION = 0xA004;
+
+
+/**
+ * No mojo.
+ */
+    static final int AL_OUT_OF_MEMORY = 0xA005;
+
+
+/** Context strings: Vendor Name. */
+    static final int AL_VENDOR = 0xB001;
+    static final int AL_VERSION = 0xB002;
+    static final int AL_RENDERER = 0xB003;
+    static final int AL_EXTENSIONS = 0xB004;
+
+/** Global tweakage. */
+
+/**
+ * Doppler scale.  Default 1.0
+ */
+    static final int AL_DOPPLER_FACTOR = 0xC000;
+
+/**
+ * Tweaks speed of propagation.
+ */
+    static final int AL_DOPPLER_VELOCITY = 0xC001;
+
+/**
+ * Speed of Sound in units per second
+ */
+    static final int AL_SPEED_OF_SOUND = 0xC003;
+
+/**
+ * Distance models
+ *
+ * used in conjunction with DistanceModel
+ *
+ * implicit: NONE, which disances distance attenuation.
+ */
+    static final int AL_DISTANCE_MODEL = 0xD000;
+    static final int AL_INVERSE_DISTANCE = 0xD001;
+    static final int AL_INVERSE_DISTANCE_CLAMPED = 0xD002;
+    static final int AL_LINEAR_DISTANCE = 0xD003;
+    static final int AL_LINEAR_DISTANCE_CLAMPED = 0xD004;
+    static final int AL_EXPONENT_DISTANCE = 0xD005;
+    static final int AL_EXPONENT_DISTANCE_CLAMPED = 0xD006;
+
+    /* ********** */
+    /* FROM efx.h */
+    /* ********** */
+
+    static final String ALC_EXT_EFX_NAME = "ALC_EXT_EFX";
+
+    static final int ALC_EFX_MAJOR_VERSION = 0x20001;
+    static final int ALC_EFX_MINOR_VERSION = 0x20002;
+    static final int ALC_MAX_AUXILIARY_SENDS  = 0x20003;
+
+
+///* Listener properties. */
+//#define AL_METERS_PER_UNIT                       0x20004
+//
+///* Source properties. */
+    static final int AL_DIRECT_FILTER = 0x20005;
+    static final int AL_AUXILIARY_SEND_FILTER = 0x20006;
+//#define AL_AIR_ABSORPTION_FACTOR                 0x20007
+//#define AL_ROOM_ROLLOFF_FACTOR                   0x20008
+//#define AL_CONE_OUTER_GAINHF                     0x20009
+    static final int AL_DIRECT_FILTER_GAINHF_AUTO = 0x2000A;
+//#define AL_AUXILIARY_SEND_FILTER_GAIN_AUTO       0x2000B
+//#define AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO     0x2000C
+//
+//
+///* Effect properties. */
+//
+///* Reverb effect parameters */
+    static final int AL_REVERB_DENSITY = 0x0001;
+    static final int AL_REVERB_DIFFUSION = 0x0002;
+    static final int AL_REVERB_GAIN = 0x0003;
+    static final int AL_REVERB_GAINHF = 0x0004;
+    static final int AL_REVERB_DECAY_TIME = 0x0005;
+    static final int AL_REVERB_DECAY_HFRATIO = 0x0006;
+    static final int AL_REVERB_REFLECTIONS_GAIN = 0x0007;
+    static final int AL_REVERB_REFLECTIONS_DELAY = 0x0008;
+    static final int AL_REVERB_LATE_REVERB_GAIN = 0x0009;
+    static final int AL_REVERB_LATE_REVERB_DELAY = 0x000A;
+    static final int AL_REVERB_AIR_ABSORPTION_GAINHF = 0x000B;
+    static final int AL_REVERB_ROOM_ROLLOFF_FACTOR = 0x000C;
+    static final int AL_REVERB_DECAY_HFLIMIT = 0x000D;
+
+///* EAX Reverb effect parameters */
+//#define AL_EAXREVERB_DENSITY                     0x0001
+//#define AL_EAXREVERB_DIFFUSION                   0x0002
+//#define AL_EAXREVERB_GAIN                        0x0003
+//#define AL_EAXREVERB_GAINHF                      0x0004
+//#define AL_EAXREVERB_GAINLF                      0x0005
+//#define AL_EAXREVERB_DECAY_TIME                  0x0006
+//#define AL_EAXREVERB_DECAY_HFRATIO               0x0007
+//#define AL_EAXREVERB_DECAY_LFRATIO               0x0008
+//#define AL_EAXREVERB_REFLECTIONS_GAIN            0x0009
+//#define AL_EAXREVERB_REFLECTIONS_DELAY           0x000A
+//#define AL_EAXREVERB_REFLECTIONS_PAN             0x000B
+//#define AL_EAXREVERB_LATE_REVERB_GAIN            0x000C
+//#define AL_EAXREVERB_LATE_REVERB_DELAY           0x000D
+//#define AL_EAXREVERB_LATE_REVERB_PAN             0x000E
+//#define AL_EAXREVERB_ECHO_TIME                   0x000F
+//#define AL_EAXREVERB_ECHO_DEPTH                  0x0010
+//#define AL_EAXREVERB_MODULATION_TIME             0x0011
+//#define AL_EAXREVERB_MODULATION_DEPTH            0x0012
+//#define AL_EAXREVERB_AIR_ABSORPTION_GAINHF       0x0013
+//#define AL_EAXREVERB_HFREFERENCE                 0x0014
+//#define AL_EAXREVERB_LFREFERENCE                 0x0015
+//#define AL_EAXREVERB_ROOM_ROLLOFF_FACTOR         0x0016
+//#define AL_EAXREVERB_DECAY_HFLIMIT               0x0017
+//
+///* Chorus effect parameters */
+//#define AL_CHORUS_WAVEFORM                       0x0001
+//#define AL_CHORUS_PHASE                          0x0002
+//#define AL_CHORUS_RATE                           0x0003
+//#define AL_CHORUS_DEPTH                          0x0004
+//#define AL_CHORUS_FEEDBACK                       0x0005
+//#define AL_CHORUS_DELAY                          0x0006
+//
+///* Distortion effect parameters */
+//#define AL_DISTORTION_EDGE                       0x0001
+//#define AL_DISTORTION_GAIN                       0x0002
+//#define AL_DISTORTION_LOWPASS_CUTOFF             0x0003
+//#define AL_DISTORTION_EQCENTER                   0x0004
+//#define AL_DISTORTION_EQBANDWIDTH                0x0005
+//
+///* Echo effect parameters */
+//#define AL_ECHO_DELAY                            0x0001
+//#define AL_ECHO_LRDELAY                          0x0002
+//#define AL_ECHO_DAMPING                          0x0003
+//#define AL_ECHO_FEEDBACK                         0x0004
+//#define AL_ECHO_SPREAD                           0x0005
+//
+///* Flanger effect parameters */
+//#define AL_FLANGER_WAVEFORM                      0x0001
+//#define AL_FLANGER_PHASE                         0x0002
+//#define AL_FLANGER_RATE                          0x0003
+//#define AL_FLANGER_DEPTH                         0x0004
+//#define AL_FLANGER_FEEDBACK                      0x0005
+//#define AL_FLANGER_DELAY                         0x0006
+//
+///* Frequency shifter effect parameters */
+//#define AL_FREQUENCY_SHIFTER_FREQUENCY           0x0001
+//#define AL_FREQUENCY_SHIFTER_LEFT_DIRECTION      0x0002
+//#define AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION     0x0003
+//
+///* Vocal morpher effect parameters */
+//#define AL_VOCAL_MORPHER_PHONEMEA                0x0001
+//#define AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING  0x0002
+//#define AL_VOCAL_MORPHER_PHONEMEB                0x0003
+//#define AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING  0x0004
+//#define AL_VOCAL_MORPHER_WAVEFORM                0x0005
+//#define AL_VOCAL_MORPHER_RATE                    0x0006
+//
+///* Pitchshifter effect parameters */
+//#define AL_PITCH_SHIFTER_COARSE_TUNE             0x0001
+//#define AL_PITCH_SHIFTER_FINE_TUNE               0x0002
+//
+///* Ringmodulator effect parameters */
+//#define AL_RING_MODULATOR_FREQUENCY              0x0001
+//#define AL_RING_MODULATOR_HIGHPASS_CUTOFF        0x0002
+//#define AL_RING_MODULATOR_WAVEFORM               0x0003
+//
+///* Autowah effect parameters */
+//#define AL_AUTOWAH_ATTACK_TIME                   0x0001
+//#define AL_AUTOWAH_RELEASE_TIME                  0x0002
+//#define AL_AUTOWAH_RESONANCE                     0x0003
+//#define AL_AUTOWAH_PEAK_GAIN                     0x0004
+//
+///* Compressor effect parameters */
+//#define AL_COMPRESSOR_ONOFF                      0x0001
+//
+///* Equalizer effect parameters */
+//#define AL_EQUALIZER_LOW_GAIN                    0x0001
+//#define AL_EQUALIZER_LOW_CUTOFF                  0x0002
+//#define AL_EQUALIZER_MID1_GAIN                   0x0003
+//#define AL_EQUALIZER_MID1_CENTER                 0x0004
+//#define AL_EQUALIZER_MID1_WIDTH                  0x0005
+//#define AL_EQUALIZER_MID2_GAIN                   0x0006
+//#define AL_EQUALIZER_MID2_CENTER                 0x0007
+//#define AL_EQUALIZER_MID2_WIDTH                  0x0008
+//#define AL_EQUALIZER_HIGH_GAIN                   0x0009
+//#define AL_EQUALIZER_HIGH_CUTOFF                 0x000A
+//
+///* Effect type */
+//#define AL_EFFECT_FIRST_PARAMETER                0x0000
+//#define AL_EFFECT_LAST_PARAMETER                 0x8000
+    static final int AL_EFFECT_TYPE = 0x8001;
+//
+///* Effect types, used with the AL_EFFECT_TYPE property */
+//#define AL_EFFECT_NULL                           0x0000
+    static final int AL_EFFECT_REVERB = 0x0001;
+//#define AL_EFFECT_CHORUS                         0x0002
+//#define AL_EFFECT_DISTORTION                     0x0003
+//#define AL_EFFECT_ECHO                           0x0004
+//#define AL_EFFECT_FLANGER                        0x0005
+//#define AL_EFFECT_FREQUENCY_SHIFTER              0x0006
+//#define AL_EFFECT_VOCAL_MORPHER                  0x0007
+//#define AL_EFFECT_PITCH_SHIFTER                  0x0008
+//#define AL_EFFECT_RING_MODULATOR                 0x0009
+//#define AL_EFFECT_AUTOWAH                        0x000A
+//#define AL_EFFECT_COMPRESSOR                     0x000B
+//#define AL_EFFECT_EQUALIZER                      0x000C
+//#define AL_EFFECT_EAXREVERB                      0x8000
+//
+///* Auxiliary Effect Slot properties. */
+    static final int AL_EFFECTSLOT_EFFECT = 0x0001;
+//#define AL_EFFECTSLOT_GAIN                       0x0002
+//#define AL_EFFECTSLOT_AUXILIARY_SEND_AUTO        0x0003
+//
+///* NULL Auxiliary Slot ID to disable a source send. */
+//#define AL_EFFECTSLOT_NULL                       0x0000
+//
+//
+///* Filter properties. */
+//
+///* Lowpass filter parameters */
+    static final int AL_LOWPASS_GAIN = 0x0001;
+    static final int AL_LOWPASS_GAINHF = 0x0002;
+//
+///* Highpass filter parameters */
+//#define AL_HIGHPASS_GAIN                         0x0001
+//#define AL_HIGHPASS_GAINLF                       0x0002
+//
+///* Bandpass filter parameters */
+//#define AL_BANDPASS_GAIN                         0x0001
+//#define AL_BANDPASS_GAINLF                       0x0002
+//#define AL_BANDPASS_GAINHF                       0x0003
+//
+///* Filter type */
+//#define AL_FILTER_FIRST_PARAMETER                0x0000
+//#define AL_FILTER_LAST_PARAMETER                 0x8000
+    static final int AL_FILTER_TYPE = 0x8001;
+//
+///* Filter types, used with the AL_FILTER_TYPE property */
+    static final int AL_FILTER_NULL = 0x0000;
+    static final int AL_FILTER_LOWPASS = 0x0001;
+    static final int AL_FILTER_HIGHPASS = 0x0002;
+//#define AL_FILTER_BANDPASS                       0x0003
+//
+///* Filter ranges and defaults. */
+//
+///* Lowpass filter */
+//#define AL_LOWPASS_MIN_GAIN                      (0.0f)
+//#define AL_LOWPASS_MAX_GAIN                      (1.0f)
+//#define AL_LOWPASS_DEFAULT_GAIN                  (1.0f)
+//
+//#define AL_LOWPASS_MIN_GAINHF                    (0.0f)
+//#define AL_LOWPASS_MAX_GAINHF                    (1.0f)
+//#define AL_LOWPASS_DEFAULT_GAINHF                (1.0f)
+//
+///* Highpass filter */
+//#define AL_HIGHPASS_MIN_GAIN                     (0.0f)
+//#define AL_HIGHPASS_MAX_GAIN                     (1.0f)
+//#define AL_HIGHPASS_DEFAULT_GAIN                 (1.0f)
+//
+//#define AL_HIGHPASS_MIN_GAINLF                   (0.0f)
+//#define AL_HIGHPASS_MAX_GAINLF                   (1.0f)
+//#define AL_HIGHPASS_DEFAULT_GAINLF               (1.0f)
+//
+///* Bandpass filter */
+//#define AL_BANDPASS_MIN_GAIN                     (0.0f)
+//#define AL_BANDPASS_MAX_GAIN                     (1.0f)
+//#define AL_BANDPASS_DEFAULT_GAIN                 (1.0f)
+//
+//#define AL_BANDPASS_MIN_GAINHF                   (0.0f)
+//#define AL_BANDPASS_MAX_GAINHF                   (1.0f)
+//#define AL_BANDPASS_DEFAULT_GAINHF               (1.0f)
+//
+//#define AL_BANDPASS_MIN_GAINLF                   (0.0f)
+//#define AL_BANDPASS_MAX_GAINLF                   (1.0f)
+//#define AL_BANDPASS_DEFAULT_GAINLF               (1.0f)
+//
+//
+///* Effect parameter ranges and defaults. */
+//
+///* Standard reverb effect */
+//#define AL_REVERB_MIN_DENSITY                    (0.0f)
+//#define AL_REVERB_MAX_DENSITY                    (1.0f)
+//#define AL_REVERB_DEFAULT_DENSITY                (1.0f)
+//
+//#define AL_REVERB_MIN_DIFFUSION                  (0.0f)
+//#define AL_REVERB_MAX_DIFFUSION                  (1.0f)
+//#define AL_REVERB_DEFAULT_DIFFUSION              (1.0f)
+//
+//#define AL_REVERB_MIN_GAIN                       (0.0f)
+//#define AL_REVERB_MAX_GAIN                       (1.0f)
+//#define AL_REVERB_DEFAULT_GAIN                   (0.32f)
+//
+//#define AL_REVERB_MIN_GAINHF                     (0.0f)
+//#define AL_REVERB_MAX_GAINHF                     (1.0f)
+//#define AL_REVERB_DEFAULT_GAINHF                 (0.89f)
+//
+//#define AL_REVERB_MIN_DECAY_TIME                 (0.1f)
+//#define AL_REVERB_MAX_DECAY_TIME                 (20.0f)
+//#define AL_REVERB_DEFAULT_DECAY_TIME             (1.49f)
+//
+//#define AL_REVERB_MIN_DECAY_HFRATIO              (0.1f)
+//#define AL_REVERB_MAX_DECAY_HFRATIO              (2.0f)
+//#define AL_REVERB_DEFAULT_DECAY_HFRATIO          (0.83f)
+//
+//#define AL_REVERB_MIN_REFLECTIONS_GAIN           (0.0f)
+//#define AL_REVERB_MAX_REFLECTIONS_GAIN           (3.16f)
+//#define AL_REVERB_DEFAULT_REFLECTIONS_GAIN       (0.05f)
+//
+//#define AL_REVERB_MIN_REFLECTIONS_DELAY          (0.0f)
+//#define AL_REVERB_MAX_REFLECTIONS_DELAY          (0.3f)
+//#define AL_REVERB_DEFAULT_REFLECTIONS_DELAY      (0.007f)
+//
+//#define AL_REVERB_MIN_LATE_REVERB_GAIN           (0.0f)
+//#define AL_REVERB_MAX_LATE_REVERB_GAIN           (10.0f)
+//#define AL_REVERB_DEFAULT_LATE_REVERB_GAIN       (1.26f)
+//
+//#define AL_REVERB_MIN_LATE_REVERB_DELAY          (0.0f)
+//#define AL_REVERB_MAX_LATE_REVERB_DELAY          (0.1f)
+//#define AL_REVERB_DEFAULT_LATE_REVERB_DELAY      (0.011f)
+//
+//#define AL_REVERB_MIN_AIR_ABSORPTION_GAINHF      (0.892f)
+//#define AL_REVERB_MAX_AIR_ABSORPTION_GAINHF      (1.0f)
+//#define AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF  (0.994f)
+//
+//#define AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR        (0.0f)
+//#define AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR        (10.0f)
+//#define AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR    (0.0f)
+//
+//#define AL_REVERB_MIN_DECAY_HFLIMIT              AL_FALSE
+//#define AL_REVERB_MAX_DECAY_HFLIMIT              AL_TRUE
+//#define AL_REVERB_DEFAULT_DECAY_HFLIMIT          AL_TRUE
+//
+///* EAX reverb effect */
+//#define AL_EAXREVERB_MIN_DENSITY                 (0.0f)
+//#define AL_EAXREVERB_MAX_DENSITY                 (1.0f)
+//#define AL_EAXREVERB_DEFAULT_DENSITY             (1.0f)
+//
+//#define AL_EAXREVERB_MIN_DIFFUSION               (0.0f)
+//#define AL_EAXREVERB_MAX_DIFFUSION               (1.0f)
+//#define AL_EAXREVERB_DEFAULT_DIFFUSION           (1.0f)
+//
+//#define AL_EAXREVERB_MIN_GAIN                    (0.0f)
+//#define AL_EAXREVERB_MAX_GAIN                    (1.0f)
+//#define AL_EAXREVERB_DEFAULT_GAIN                (0.32f)
+//
+//#define AL_EAXREVERB_MIN_GAINHF                  (0.0f)
+//#define AL_EAXREVERB_MAX_GAINHF                  (1.0f)
+//#define AL_EAXREVERB_DEFAULT_GAINHF              (0.89f)
+//
+//#define AL_EAXREVERB_MIN_GAINLF                  (0.0f)
+//#define AL_EAXREVERB_MAX_GAINLF                  (1.0f)
+//#define AL_EAXREVERB_DEFAULT_GAINLF              (1.0f)
+//
+//#define AL_EAXREVERB_MIN_DECAY_TIME              (0.1f)
+//#define AL_EAXREVERB_MAX_DECAY_TIME              (20.0f)
+//#define AL_EAXREVERB_DEFAULT_DECAY_TIME          (1.49f)
+//
+//#define AL_EAXREVERB_MIN_DECAY_HFRATIO           (0.1f)
+//#define AL_EAXREVERB_MAX_DECAY_HFRATIO           (2.0f)
+//#define AL_EAXREVERB_DEFAULT_DECAY_HFRATIO       (0.83f)
+//
+//#define AL_EAXREVERB_MIN_DECAY_LFRATIO           (0.1f)
+//#define AL_EAXREVERB_MAX_DECAY_LFRATIO           (2.0f)
+//#define AL_EAXREVERB_DEFAULT_DECAY_LFRATIO       (1.0f)
+//
+//#define AL_EAXREVERB_MIN_REFLECTIONS_GAIN        (0.0f)
+//#define AL_EAXREVERB_MAX_REFLECTIONS_GAIN        (3.16f)
+//#define AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN    (0.05f)
+//
+//#define AL_EAXREVERB_MIN_REFLECTIONS_DELAY       (0.0f)
+//#define AL_EAXREVERB_MAX_REFLECTIONS_DELAY       (0.3f)
+//#define AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY   (0.007f)
+//
+//#define AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ (0.0f)
+//
+//#define AL_EAXREVERB_MIN_LATE_REVERB_GAIN        (0.0f)
+//#define AL_EAXREVERB_MAX_LATE_REVERB_GAIN        (10.0f)
+//#define AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN    (1.26f)
+//
+//#define AL_EAXREVERB_MIN_LATE_REVERB_DELAY       (0.0f)
+//#define AL_EAXREVERB_MAX_LATE_REVERB_DELAY       (0.1f)
+//#define AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY   (0.011f)
+//
+//#define AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ (0.0f)
+//
+//#define AL_EAXREVERB_MIN_ECHO_TIME               (0.075f)
+//#define AL_EAXREVERB_MAX_ECHO_TIME               (0.25f)
+//#define AL_EAXREVERB_DEFAULT_ECHO_TIME           (0.25f)
+//
+//#define AL_EAXREVERB_MIN_ECHO_DEPTH              (0.0f)
+//#define AL_EAXREVERB_MAX_ECHO_DEPTH              (1.0f)
+//#define AL_EAXREVERB_DEFAULT_ECHO_DEPTH          (0.0f)
+//
+//#define AL_EAXREVERB_MIN_MODULATION_TIME         (0.04f)
+//#define AL_EAXREVERB_MAX_MODULATION_TIME         (4.0f)
+//#define AL_EAXREVERB_DEFAULT_MODULATION_TIME     (0.25f)
+//
+//#define AL_EAXREVERB_MIN_MODULATION_DEPTH        (0.0f)
+//#define AL_EAXREVERB_MAX_MODULATION_DEPTH        (1.0f)
+//#define AL_EAXREVERB_DEFAULT_MODULATION_DEPTH    (0.0f)
+//
+//#define AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF   (0.892f)
+//#define AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF   (1.0f)
+//#define AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f)
+//
+//#define AL_EAXREVERB_MIN_HFREFERENCE             (1000.0f)
+//#define AL_EAXREVERB_MAX_HFREFERENCE             (20000.0f)
+//#define AL_EAXREVERB_DEFAULT_HFREFERENCE         (5000.0f)
+//
+//#define AL_EAXREVERB_MIN_LFREFERENCE             (20.0f)
+//#define AL_EAXREVERB_MAX_LFREFERENCE             (1000.0f)
+//#define AL_EAXREVERB_DEFAULT_LFREFERENCE         (250.0f)
+//
+//#define AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR     (0.0f)
+//#define AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR     (10.0f)
+//#define AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f)
+//
+//#define AL_EAXREVERB_MIN_DECAY_HFLIMIT           AL_FALSE
+//#define AL_EAXREVERB_MAX_DECAY_HFLIMIT           AL_TRUE
+//#define AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT       AL_TRUE
+//
+///* Chorus effect */
+//#define AL_CHORUS_WAVEFORM_SINUSOID              (0)
+//#define AL_CHORUS_WAVEFORM_TRIANGLE              (1)
+//
+//#define AL_CHORUS_MIN_WAVEFORM                   (0)
+//#define AL_CHORUS_MAX_WAVEFORM                   (1)
+//#define AL_CHORUS_DEFAULT_WAVEFORM               (1)
+//
+//#define AL_CHORUS_MIN_PHASE                      (-180)
+//#define AL_CHORUS_MAX_PHASE                      (180)
+//#define AL_CHORUS_DEFAULT_PHASE                  (90)
+//
+//#define AL_CHORUS_MIN_RATE                       (0.0f)
+//#define AL_CHORUS_MAX_RATE                       (10.0f)
+//#define AL_CHORUS_DEFAULT_RATE                   (1.1f)
+//
+//#define AL_CHORUS_MIN_DEPTH                      (0.0f)
+//#define AL_CHORUS_MAX_DEPTH                      (1.0f)
+//#define AL_CHORUS_DEFAULT_DEPTH                  (0.1f)
+//
+//#define AL_CHORUS_MIN_FEEDBACK                   (-1.0f)
+//#define AL_CHORUS_MAX_FEEDBACK                   (1.0f)
+//#define AL_CHORUS_DEFAULT_FEEDBACK               (0.25f)
+//
+//#define AL_CHORUS_MIN_DELAY                      (0.0f)
+//#define AL_CHORUS_MAX_DELAY                      (0.016f)
+//#define AL_CHORUS_DEFAULT_DELAY                  (0.016f)
+//
+///* Distortion effect */
+//#define AL_DISTORTION_MIN_EDGE                   (0.0f)
+//#define AL_DISTORTION_MAX_EDGE                   (1.0f)
+//#define AL_DISTORTION_DEFAULT_EDGE               (0.2f)
+//
+//#define AL_DISTORTION_MIN_GAIN                   (0.01f)
+//#define AL_DISTORTION_MAX_GAIN                   (1.0f)
+//#define AL_DISTORTION_DEFAULT_GAIN               (0.05f)
+//
+//#define AL_DISTORTION_MIN_LOWPASS_CUTOFF         (80.0f)
+//#define AL_DISTORTION_MAX_LOWPASS_CUTOFF         (24000.0f)
+//#define AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF     (8000.0f)
+//
+//#define AL_DISTORTION_MIN_EQCENTER               (80.0f)
+//#define AL_DISTORTION_MAX_EQCENTER               (24000.0f)
+//#define AL_DISTORTION_DEFAULT_EQCENTER           (3600.0f)
+//
+//#define AL_DISTORTION_MIN_EQBANDWIDTH            (80.0f)
+//#define AL_DISTORTION_MAX_EQBANDWIDTH            (24000.0f)
+//#define AL_DISTORTION_DEFAULT_EQBANDWIDTH        (3600.0f)
+//
+///* Echo effect */
+//#define AL_ECHO_MIN_DELAY                        (0.0f)
+//#define AL_ECHO_MAX_DELAY                        (0.207f)
+//#define AL_ECHO_DEFAULT_DELAY                    (0.1f)
+//
+//#define AL_ECHO_MIN_LRDELAY                      (0.0f)
+//#define AL_ECHO_MAX_LRDELAY                      (0.404f)
+//#define AL_ECHO_DEFAULT_LRDELAY                  (0.1f)
+//
+//#define AL_ECHO_MIN_DAMPING                      (0.0f)
+//#define AL_ECHO_MAX_DAMPING                      (0.99f)
+//#define AL_ECHO_DEFAULT_DAMPING                  (0.5f)
+//
+//#define AL_ECHO_MIN_FEEDBACK                     (0.0f)
+//#define AL_ECHO_MAX_FEEDBACK                     (1.0f)
+//#define AL_ECHO_DEFAULT_FEEDBACK                 (0.5f)
+//
+//#define AL_ECHO_MIN_SPREAD                       (-1.0f)
+//#define AL_ECHO_MAX_SPREAD                       (1.0f)
+//#define AL_ECHO_DEFAULT_SPREAD                   (-1.0f)
+//
+///* Flanger effect */
+//#define AL_FLANGER_WAVEFORM_SINUSOID             (0)
+//#define AL_FLANGER_WAVEFORM_TRIANGLE             (1)
+//
+//#define AL_FLANGER_MIN_WAVEFORM                  (0)
+//#define AL_FLANGER_MAX_WAVEFORM                  (1)
+//#define AL_FLANGER_DEFAULT_WAVEFORM              (1)
+//
+//#define AL_FLANGER_MIN_PHASE                     (-180)
+//#define AL_FLANGER_MAX_PHASE                     (180)
+//#define AL_FLANGER_DEFAULT_PHASE                 (0)
+//
+//#define AL_FLANGER_MIN_RATE                      (0.0f)
+//#define AL_FLANGER_MAX_RATE                      (10.0f)
+//#define AL_FLANGER_DEFAULT_RATE                  (0.27f)
+//
+//#define AL_FLANGER_MIN_DEPTH                     (0.0f)
+//#define AL_FLANGER_MAX_DEPTH                     (1.0f)
+//#define AL_FLANGER_DEFAULT_DEPTH                 (1.0f)
+//
+//#define AL_FLANGER_MIN_FEEDBACK                  (-1.0f)
+//#define AL_FLANGER_MAX_FEEDBACK                  (1.0f)
+//#define AL_FLANGER_DEFAULT_FEEDBACK              (-0.5f)
+//
+//#define AL_FLANGER_MIN_DELAY                     (0.0f)
+//#define AL_FLANGER_MAX_DELAY                     (0.004f)
+//#define AL_FLANGER_DEFAULT_DELAY                 (0.002f)
+//
+///* Frequency shifter effect */
+//#define AL_FREQUENCY_SHIFTER_MIN_FREQUENCY       (0.0f)
+//#define AL_FREQUENCY_SHIFTER_MAX_FREQUENCY       (24000.0f)
+//#define AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY   (0.0f)
+//
+//#define AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION  (0)
+//#define AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION  (2)
+//#define AL_FREQUENCY_SHIFTER_DEFAULT_LEFT_DIRECTION (0)
+//
+//#define AL_FREQUENCY_SHIFTER_DIRECTION_DOWN      (0)
+//#define AL_FREQUENCY_SHIFTER_DIRECTION_UP        (1)
+//#define AL_FREQUENCY_SHIFTER_DIRECTION_OFF       (2)
+//
+//#define AL_FREQUENCY_SHIFTER_MIN_RIGHT_DIRECTION (0)
+//#define AL_FREQUENCY_SHIFTER_MAX_RIGHT_DIRECTION (2)
+//#define AL_FREQUENCY_SHIFTER_DEFAULT_RIGHT_DIRECTION (0)
+//
+///* Vocal morpher effect */
+//#define AL_VOCAL_MORPHER_MIN_PHONEMEA            (0)
+//#define AL_VOCAL_MORPHER_MAX_PHONEMEA            (29)
+//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA        (0)
+//
+//#define AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING (-24)
+//#define AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING (24)
+//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA_COARSE_TUNING (0)
+//
+//#define AL_VOCAL_MORPHER_MIN_PHONEMEB            (0)
+//#define AL_VOCAL_MORPHER_MAX_PHONEMEB            (29)
+//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB        (10)
+//
+//#define AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING (-24)
+//#define AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING (24)
+//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB_COARSE_TUNING (0)
+//
+//#define AL_VOCAL_MORPHER_PHONEME_A               (0)
+//#define AL_VOCAL_MORPHER_PHONEME_E               (1)
+//#define AL_VOCAL_MORPHER_PHONEME_I               (2)
+//#define AL_VOCAL_MORPHER_PHONEME_O               (3)
+//#define AL_VOCAL_MORPHER_PHONEME_U               (4)
+//#define AL_VOCAL_MORPHER_PHONEME_AA              (5)
+//#define AL_VOCAL_MORPHER_PHONEME_AE              (6)
+//#define AL_VOCAL_MORPHER_PHONEME_AH              (7)
+//#define AL_VOCAL_MORPHER_PHONEME_AO              (8)
+//#define AL_VOCAL_MORPHER_PHONEME_EH              (9)
+//#define AL_VOCAL_MORPHER_PHONEME_ER              (10)
+//#define AL_VOCAL_MORPHER_PHONEME_IH              (11)
+//#define AL_VOCAL_MORPHER_PHONEME_IY              (12)
+//#define AL_VOCAL_MORPHER_PHONEME_UH              (13)
+//#define AL_VOCAL_MORPHER_PHONEME_UW              (14)
+//#define AL_VOCAL_MORPHER_PHONEME_B               (15)
+//#define AL_VOCAL_MORPHER_PHONEME_D               (16)
+//#define AL_VOCAL_MORPHER_PHONEME_F               (17)
+//#define AL_VOCAL_MORPHER_PHONEME_G               (18)
+//#define AL_VOCAL_MORPHER_PHONEME_J               (19)
+//#define AL_VOCAL_MORPHER_PHONEME_K               (20)
+//#define AL_VOCAL_MORPHER_PHONEME_L               (21)
+//#define AL_VOCAL_MORPHER_PHONEME_M               (22)
+//#define AL_VOCAL_MORPHER_PHONEME_N               (23)
+//#define AL_VOCAL_MORPHER_PHONEME_P               (24)
+//#define AL_VOCAL_MORPHER_PHONEME_R               (25)
+//#define AL_VOCAL_MORPHER_PHONEME_S               (26)
+//#define AL_VOCAL_MORPHER_PHONEME_T               (27)
+//#define AL_VOCAL_MORPHER_PHONEME_V               (28)
+//#define AL_VOCAL_MORPHER_PHONEME_Z               (29)
+//
+//#define AL_VOCAL_MORPHER_WAVEFORM_SINUSOID       (0)
+//#define AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE       (1)
+//#define AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH       (2)
+//
+//#define AL_VOCAL_MORPHER_MIN_WAVEFORM            (0)
+//#define AL_VOCAL_MORPHER_MAX_WAVEFORM            (2)
+//#define AL_VOCAL_MORPHER_DEFAULT_WAVEFORM        (0)
+//
+//#define AL_VOCAL_MORPHER_MIN_RATE                (0.0f)
+//#define AL_VOCAL_MORPHER_MAX_RATE                (10.0f)
+//#define AL_VOCAL_MORPHER_DEFAULT_RATE            (1.41f)
+//
+///* Pitch shifter effect */
+//#define AL_PITCH_SHIFTER_MIN_COARSE_TUNE         (-12)
+//#define AL_PITCH_SHIFTER_MAX_COARSE_TUNE         (12)
+//#define AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE     (12)
+//
+//#define AL_PITCH_SHIFTER_MIN_FINE_TUNE           (-50)
+//#define AL_PITCH_SHIFTER_MAX_FINE_TUNE           (50)
+//#define AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE       (0)
+//
+///* Ring modulator effect */
+//#define AL_RING_MODULATOR_MIN_FREQUENCY          (0.0f)
+//#define AL_RING_MODULATOR_MAX_FREQUENCY          (8000.0f)
+//#define AL_RING_MODULATOR_DEFAULT_FREQUENCY      (440.0f)
+//
+//#define AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF    (0.0f)
+//#define AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF    (24000.0f)
+//#define AL_RING_MODULATOR_DEFAULT_HIGHPASS_CUTOFF (800.0f)
+//
+//#define AL_RING_MODULATOR_SINUSOID               (0)
+//#define AL_RING_MODULATOR_SAWTOOTH               (1)
+//#define AL_RING_MODULATOR_SQUARE                 (2)
+//
+//#define AL_RING_MODULATOR_MIN_WAVEFORM           (0)
+//#define AL_RING_MODULATOR_MAX_WAVEFORM           (2)
+//#define AL_RING_MODULATOR_DEFAULT_WAVEFORM       (0)
+//
+///* Autowah effect */
+//#define AL_AUTOWAH_MIN_ATTACK_TIME               (0.0001f)
+//#define AL_AUTOWAH_MAX_ATTACK_TIME               (1.0f)
+//#define AL_AUTOWAH_DEFAULT_ATTACK_TIME           (0.06f)
+//
+//#define AL_AUTOWAH_MIN_RELEASE_TIME              (0.0001f)
+//#define AL_AUTOWAH_MAX_RELEASE_TIME              (1.0f)
+//#define AL_AUTOWAH_DEFAULT_RELEASE_TIME          (0.06f)
+//
+//#define AL_AUTOWAH_MIN_RESONANCE                 (2.0f)
+//#define AL_AUTOWAH_MAX_RESONANCE                 (1000.0f)
+//#define AL_AUTOWAH_DEFAULT_RESONANCE             (1000.0f)
+//
+//#define AL_AUTOWAH_MIN_PEAK_GAIN                 (0.00003f)
+//#define AL_AUTOWAH_MAX_PEAK_GAIN                 (31621.0f)
+//#define AL_AUTOWAH_DEFAULT_PEAK_GAIN             (11.22f)
+//
+///* Compressor effect */
+//#define AL_COMPRESSOR_MIN_ONOFF                  (0)
+//#define AL_COMPRESSOR_MAX_ONOFF                  (1)
+//#define AL_COMPRESSOR_DEFAULT_ONOFF              (1)
+//
+///* Equalizer effect */
+//#define AL_EQUALIZER_MIN_LOW_GAIN                (0.126f)
+//#define AL_EQUALIZER_MAX_LOW_GAIN                (7.943f)
+//#define AL_EQUALIZER_DEFAULT_LOW_GAIN            (1.0f)
+//
+//#define AL_EQUALIZER_MIN_LOW_CUTOFF              (50.0f)
+//#define AL_EQUALIZER_MAX_LOW_CUTOFF              (800.0f)
+//#define AL_EQUALIZER_DEFAULT_LOW_CUTOFF          (200.0f)
+//
+//#define AL_EQUALIZER_MIN_MID1_GAIN               (0.126f)
+//#define AL_EQUALIZER_MAX_MID1_GAIN               (7.943f)
+//#define AL_EQUALIZER_DEFAULT_MID1_GAIN           (1.0f)
+//
+//#define AL_EQUALIZER_MIN_MID1_CENTER             (200.0f)
+//#define AL_EQUALIZER_MAX_MID1_CENTER             (3000.0f)
+//#define AL_EQUALIZER_DEFAULT_MID1_CENTER         (500.0f)
+//
+//#define AL_EQUALIZER_MIN_MID1_WIDTH              (0.01f)
+//#define AL_EQUALIZER_MAX_MID1_WIDTH              (1.0f)
+//#define AL_EQUALIZER_DEFAULT_MID1_WIDTH          (1.0f)
+//
+//#define AL_EQUALIZER_MIN_MID2_GAIN               (0.126f)
+//#define AL_EQUALIZER_MAX_MID2_GAIN               (7.943f)
+//#define AL_EQUALIZER_DEFAULT_MID2_GAIN           (1.0f)
+//
+//#define AL_EQUALIZER_MIN_MID2_CENTER             (1000.0f)
+//#define AL_EQUALIZER_MAX_MID2_CENTER             (8000.0f)
+//#define AL_EQUALIZER_DEFAULT_MID2_CENTER         (3000.0f)
+//
+//#define AL_EQUALIZER_MIN_MID2_WIDTH              (0.01f)
+//#define AL_EQUALIZER_MAX_MID2_WIDTH              (1.0f)
+//#define AL_EQUALIZER_DEFAULT_MID2_WIDTH          (1.0f)
+//
+//#define AL_EQUALIZER_MIN_HIGH_GAIN               (0.126f)
+//#define AL_EQUALIZER_MAX_HIGH_GAIN               (7.943f)
+//#define AL_EQUALIZER_DEFAULT_HIGH_GAIN           (1.0f)
+//
+//#define AL_EQUALIZER_MIN_HIGH_CUTOFF             (4000.0f)
+//#define AL_EQUALIZER_MAX_HIGH_CUTOFF             (16000.0f)
+//#define AL_EQUALIZER_DEFAULT_HIGH_CUTOFF         (6000.0f)
+//
+//
+///* Source parameter value ranges and defaults. */
+//#define AL_MIN_AIR_ABSORPTION_FACTOR             (0.0f)
+//#define AL_MAX_AIR_ABSORPTION_FACTOR             (10.0f)
+//#define AL_DEFAULT_AIR_ABSORPTION_FACTOR         (0.0f)
+//
+//#define AL_MIN_ROOM_ROLLOFF_FACTOR               (0.0f)
+//#define AL_MAX_ROOM_ROLLOFF_FACTOR               (10.0f)
+//#define AL_DEFAULT_ROOM_ROLLOFF_FACTOR           (0.0f)
+//
+//#define AL_MIN_CONE_OUTER_GAINHF                 (0.0f)
+//#define AL_MAX_CONE_OUTER_GAINHF                 (1.0f)
+//#define AL_DEFAULT_CONE_OUTER_GAINHF             (1.0f)
+//
+//#define AL_MIN_DIRECT_FILTER_GAINHF_AUTO         AL_FALSE
+//#define AL_MAX_DIRECT_FILTER_GAINHF_AUTO         AL_TRUE
+//#define AL_DEFAULT_DIRECT_FILTER_GAINHF_AUTO     AL_TRUE
+//
+//#define AL_MIN_AUXILIARY_SEND_FILTER_GAIN_AUTO   AL_FALSE
+//#define AL_MAX_AUXILIARY_SEND_FILTER_GAIN_AUTO   AL_TRUE
+//#define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE
+//
+//#define AL_MIN_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_FALSE
+//#define AL_MAX_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE
+//#define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE
+//
+//
+///* Listener parameter value ranges and defaults. */
+//#define AL_MIN_METERS_PER_UNIT                   FLT_MIN
+//#define AL_MAX_METERS_PER_UNIT                   FLT_MAX
+//#define AL_DEFAULT_METERS_PER_UNIT               (1.0f)
+
+
+    public static String GetALErrorMsg(int errorCode) {
+        String errorText;
+        switch (errorCode) {
+            case AL_NO_ERROR:
+                errorText = "No Error";
+                break;
+            case AL_INVALID_NAME:
+                errorText = "Invalid Name";
+                break;
+            case AL_INVALID_ENUM:
+                errorText = "Invalid Enum";
+                break;
+            case AL_INVALID_VALUE:
+                errorText = "Invalid Value";
+                break;
+            case AL_INVALID_OPERATION:
+                errorText = "Invalid Operation";
+                break;
+            case AL_OUT_OF_MEMORY:
+                errorText = "Out of Memory";
+                break;
+            default:
+                errorText = "Unknown Error Code: " + String.valueOf(errorCode);
+        }
+        return errorText;
+    }
+}
+

+ 67 - 0
jme3-android/src/main/java/com/jme3/audio/android/AndroidAudioData.java

@@ -0,0 +1,67 @@
+package com.jme3.audio.android;
+
+import com.jme3.asset.AssetKey;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.util.NativeObject;
+
+public class AndroidAudioData extends AudioData {
+
+    protected AssetKey<?> assetKey;
+    protected float currentVolume = 0f;
+
+    public AndroidAudioData(){
+        super();
+    }
+    
+    protected AndroidAudioData(int id){
+        super(id);
+    }
+    
+    public AssetKey<?> getAssetKey() {
+        return assetKey;
+    }
+
+    public void setAssetKey(AssetKey<?> assetKey) {
+        this.assetKey = assetKey;
+    }
+
+    @Override
+    public DataType getDataType() {
+        return DataType.Buffer;
+    }
+
+    @Override
+    public float getDuration() {
+        return 0; // TODO: ???
+    }
+
+    @Override
+    public void resetObject() {
+        this.id = -1;
+        setUpdateNeeded();  
+    }
+
+    @Override
+    public void deleteObject(Object rendererObject) {
+        ((AudioRenderer)rendererObject).deleteAudioData(this);
+    }
+
+    public float getCurrentVolume() {
+        return currentVolume;
+    }
+
+    public void setCurrentVolume(float currentVolume) {
+        this.currentVolume = currentVolume;
+    }
+
+    @Override
+    public NativeObject createDestructableClone() {
+        return new AndroidAudioData(id);
+    }
+
+    @Override
+    public long getUniqueId() {
+        return ((long)OBJTYPE_AUDIOBUFFER << 32) | ((long)id);
+    }
+}

+ 24 - 0
jme3-android/src/main/java/com/jme3/audio/android/AndroidAudioRenderer.java

@@ -0,0 +1,24 @@
+package com.jme3.audio.android;
+
+import com.jme3.audio.AudioRenderer;
+
+/**
+ * Android specific AudioRenderer interface that supports pausing and resuming
+ * audio files when the app is minimized or placed in the background
+ *
+ * @author iwgeric
+ */
+public interface AndroidAudioRenderer extends AudioRenderer {
+
+    /**
+     * Pauses all Playing audio. To be used when the app is placed in the
+     * background.
+     */
+    public void pauseAll();
+
+    /**
+     * Resumes all Paused audio. To be used when the app is brought back to
+     * the foreground.
+     */
+    public void resumeAll();
+}

+ 523 - 0
jme3-android/src/main/java/com/jme3/audio/android/AndroidMediaPlayerAudioRenderer.java

@@ -0,0 +1,523 @@
+/*
+ * Copyright (c) 2009-2012 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.audio.android;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.SoundPool;
+import com.jme3.asset.AssetKey;
+import com.jme3.audio.*;
+import com.jme3.audio.AudioSource.Status;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class is the android implementation for {@link AudioRenderer}
+ *
+ * @author larynx
+ * @author plan_rich
+ */
+public class AndroidMediaPlayerAudioRenderer implements AndroidAudioRenderer,
+        SoundPool.OnLoadCompleteListener, MediaPlayer.OnCompletionListener {
+
+    private static final Logger logger = Logger.getLogger(AndroidMediaPlayerAudioRenderer.class.getName());
+    private final static int MAX_NUM_CHANNELS = 16;
+    private final HashMap<AudioSource, MediaPlayer> musicPlaying = new HashMap<AudioSource, MediaPlayer>();
+    private SoundPool soundPool = null;
+    private final Vector3f listenerPosition = new Vector3f();
+    // For temp use
+    private final Vector3f distanceVector = new Vector3f();
+    private final AssetManager assetManager;
+    private HashMap<Integer, AudioSource> soundpoolStillLoading = new HashMap<Integer, AudioSource>();
+    private Listener listener;
+    private boolean audioDisabled = false;
+    private final AudioManager manager;
+
+    public AndroidMediaPlayerAudioRenderer(Activity context) {
+        manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        context.setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        assetManager = context.getAssets();
+    }
+
+    @Override
+    public void initialize() {
+        soundPool = new SoundPool(MAX_NUM_CHANNELS, AudioManager.STREAM_MUSIC,
+                0);
+        soundPool.setOnLoadCompleteListener(this);
+    }
+
+    @Override
+    public void updateSourceParam(AudioSource src, AudioParam param) {
+        if (audioDisabled) {
+            return;
+        }
+
+        if (src.getChannel() < 0) {
+            return;
+        }
+
+        switch (param) {
+            case Position:
+                if (!src.isPositional()) {
+                    return;
+                }
+
+                Vector3f pos = src.getPosition();
+                break;
+            case Velocity:
+                if (!src.isPositional()) {
+                    return;
+                }
+
+                Vector3f vel = src.getVelocity();
+                break;
+            case MaxDistance:
+                if (!src.isPositional()) {
+                    return;
+                }
+                break;
+            case RefDistance:
+                if (!src.isPositional()) {
+                    return;
+                }
+                break;
+            case ReverbFilter:
+                if (!src.isPositional() || !src.isReverbEnabled()) {
+                    return;
+                }
+                break;
+            case ReverbEnabled:
+                if (!src.isPositional()) {
+                    return;
+                }
+
+                if (src.isReverbEnabled()) {
+                    updateSourceParam(src, AudioParam.ReverbFilter);
+                }
+                break;
+            case IsPositional:
+                break;
+            case Direction:
+                if (!src.isDirectional()) {
+                    return;
+                }
+
+                Vector3f dir = src.getDirection();
+                break;
+            case InnerAngle:
+                if (!src.isDirectional()) {
+                    return;
+                }
+                break;
+            case OuterAngle:
+                if (!src.isDirectional()) {
+                    return;
+                }
+                break;
+            case IsDirectional:
+                if (src.isDirectional()) {
+                    updateSourceParam(src, AudioParam.Direction);
+                    updateSourceParam(src, AudioParam.InnerAngle);
+                    updateSourceParam(src, AudioParam.OuterAngle);
+                } else {
+                }
+                break;
+            case DryFilter:
+                if (src.getDryFilter() != null) {
+                    Filter f = src.getDryFilter();
+                    if (f.isUpdateNeeded()) {
+                        // updateFilter(f);
+                    }
+                }
+                break;
+            case Looping:
+                if (src.isLooping()) {
+                }
+                break;
+            case Volume:
+                MediaPlayer mp = musicPlaying.get(src);
+                if (mp != null) {
+                    mp.setVolume(src.getVolume(), src.getVolume());
+                } else {
+                    soundPool.setVolume(src.getChannel(), src.getVolume(),
+                            src.getVolume());
+                }
+
+                break;
+            case Pitch:
+
+                break;
+        }
+
+    }
+
+    @Override
+    public void updateListenerParam(Listener listener, ListenerParam param) {
+        if (audioDisabled) {
+            return;
+        }
+
+        switch (param) {
+            case Position:
+                listenerPosition.set(listener.getLocation());
+                break;
+            case Rotation:
+                Vector3f dir = listener.getDirection();
+                Vector3f up = listener.getUp();
+
+                break;
+            case Velocity:
+                Vector3f vel = listener.getVelocity();
+
+                break;
+            case Volume:
+                // alListenerf(AL_GAIN, listener.getVolume());
+                break;
+        }
+
+    }
+
+    @Override
+    public void update(float tpf) {
+        float distance;
+        float volume;
+
+        // Loop over all mediaplayers
+        for (AudioSource src : musicPlaying.keySet()) {
+
+            MediaPlayer mp = musicPlaying.get(src);
+
+            // Calc the distance to the listener
+            distanceVector.set(listenerPosition);
+            distanceVector.subtractLocal(src.getPosition());
+            distance = FastMath.abs(distanceVector.length());
+
+            if (distance < src.getRefDistance()) {
+                distance = src.getRefDistance();
+            }
+            if (distance > src.getMaxDistance()) {
+                distance = src.getMaxDistance();
+            }
+            volume = src.getRefDistance() / distance;
+
+            AndroidAudioData audioData = (AndroidAudioData) src.getAudioData();
+
+            if (FastMath.abs(audioData.getCurrentVolume() - volume) > FastMath.FLT_EPSILON) {
+                // Left / Right channel get the same volume by now, only
+                // positional
+                mp.setVolume(volume, volume);
+
+                audioData.setCurrentVolume(volume);
+            }
+
+        }
+    }
+
+    public void setListener(Listener listener) {
+        if (audioDisabled) {
+            return;
+        }
+
+        if (this.listener != null) {
+            // previous listener no longer associated with current
+            // renderer
+            this.listener.setRenderer(null);
+        }
+
+        this.listener = listener;
+        this.listener.setRenderer(this);
+
+    }
+
+    @Override
+    public void cleanup() {
+        // Cleanup sound pool
+        if (soundPool != null) {
+            soundPool.release();
+            soundPool = null;
+        }
+
+        // Cleanup media player
+        for (AudioSource src : musicPlaying.keySet()) {
+            MediaPlayer mp = musicPlaying.get(src);
+            {
+                mp.stop();
+                mp.release();
+                src.setStatus(Status.Stopped);
+            }
+        }
+        musicPlaying.clear();
+    }
+
+    @Override
+    public void onCompletion(MediaPlayer mp) {
+        if (mp.isPlaying()) {
+            mp.seekTo(0);
+            mp.stop();
+        }
+            // XXX: This has bad performance -> maybe change overall structure of
+            // mediaplayer in this audiorenderer?
+            for (AudioSource src : musicPlaying.keySet()) {
+                if (musicPlaying.get(src) == mp) {
+                    src.setStatus(Status.Stopped);
+                    break;
+                }
+            }
+
+    }
+
+    /**
+     * Plays using the {@link SoundPool} of Android. Due to hard limitation of
+     * the SoundPool: After playing more instances of the sound you only have
+     * the channel of the last played instance.
+     *
+     * It is not possible to get information about the state of the soundpool of
+     * a specific streamid, so removing is not possilbe -> noone knows when
+     * sound finished.
+     */
+    public void playSourceInstance(AudioSource src) {
+        if (audioDisabled) {
+            return;
+        }
+
+        AndroidAudioData audioData = (AndroidAudioData) src.getAudioData();
+
+        if (!(audioData.getAssetKey() instanceof AudioKey)) {
+            throw new IllegalArgumentException("Asset is not a AudioKey");
+        }
+
+        AudioKey assetKey = (AudioKey) audioData.getAssetKey();
+
+        try {
+
+            if (audioData.getId() < 0) { // found something to load
+                int soundId = soundPool.load(
+                        assetManager.openFd(assetKey.getName()), 1);
+                audioData.setId(soundId);
+            }
+
+            int channel = soundPool.play(audioData.getId(), 1f, 1f, 1, 0, 1f);
+
+            if (channel == 0) {
+                soundpoolStillLoading.put(audioData.getId(), src);
+            } else {
+                if (src.getStatus() != Status.Stopped) {
+                    soundPool.stop(channel);
+                    src.setStatus(Status.Stopped);
+                }
+                src.setChannel(channel); // receive a channel at the last
+                setSourceParams(src);
+                // playing at least
+
+
+            }
+        } catch (IOException e) {
+            logger.log(Level.SEVERE,
+                    "Failed to load sound " + assetKey.getName(), e);
+            audioData.setId(-1);
+        }
+    }
+
+    @Override
+    public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
+        AudioSource src = soundpoolStillLoading.remove(sampleId);
+
+        if (src == null) {
+            logger.warning("Something went terribly wrong! onLoadComplete"
+                    + " had sampleId which was not in the HashMap of loading items");
+            return;
+        }
+
+        AudioData audioData = src.getAudioData();
+
+        // load was successfull
+        if (status == 0) {
+            int channelIndex;
+            channelIndex = soundPool.play(audioData.getId(), 1f, 1f, 1, 0, 1f);
+            src.setChannel(channelIndex);
+            setSourceParams(src);
+        }
+    }
+
+    public void playSource(AudioSource src) {
+        if (audioDisabled) {
+            return;
+        }
+
+        AndroidAudioData audioData = (AndroidAudioData) src.getAudioData();
+
+        MediaPlayer mp = musicPlaying.get(src);
+        if (mp == null) {
+            mp = new MediaPlayer();
+            mp.setOnCompletionListener(this);
+            mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
+        }
+
+        try {
+            if (src.getStatus() == Status.Stopped) {
+                mp.reset();
+                AssetKey<?> key = audioData.getAssetKey();
+
+                AssetFileDescriptor afd = assetManager.openFd(key.getName()); // assetKey.getName()
+                mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
+                        afd.getLength());
+                mp.prepare();
+                setSourceParams(src, mp);
+                src.setChannel(0);
+                src.setStatus(Status.Playing);
+                musicPlaying.put(src, mp);
+                mp.start();
+            } else {
+                mp.start();
+            }
+        } catch (IllegalStateException e) {
+            e.printStackTrace();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void setSourceParams(AudioSource src, MediaPlayer mp) {
+        mp.setLooping(src.isLooping());
+        mp.setVolume(src.getVolume(), src.getVolume());
+        //src.getDryFilter();
+    }
+
+    private void setSourceParams(AudioSource src) {
+        soundPool.setLoop(src.getChannel(), src.isLooping() ? -1 : 0);
+        soundPool.setVolume(src.getChannel(), src.getVolume(), src.getVolume());
+    }
+
+    /**
+     * Pause the current playing sounds. Both from the {@link SoundPool} and the
+     * active {@link MediaPlayer}s
+     */
+    public void pauseAll() {
+        if (soundPool != null) {
+            soundPool.autoPause();
+            for (MediaPlayer mp : musicPlaying.values()) {
+                if(mp.isPlaying()){
+                    mp.pause();
+                }
+            }
+        }
+    }
+
+    /**
+     * Resume all paused sounds.
+     */
+    public void resumeAll() {
+        if (soundPool != null) {
+            soundPool.autoResume();
+            for (MediaPlayer mp : musicPlaying.values()) {
+                mp.start(); //no resume -> api says call start to resume
+            }
+        }
+    }
+
+    public void pauseSource(AudioSource src) {
+        if (audioDisabled) {
+            return;
+        }
+
+        MediaPlayer mp = musicPlaying.get(src);
+        if (mp != null) {
+            mp.pause();
+            src.setStatus(Status.Paused);
+        } else {
+            int channel = src.getChannel();
+            if (channel != -1) {
+                soundPool.pause(channel); // is not very likley to make
+            }											// something useful :)
+        }
+    }
+
+    public void stopSource(AudioSource src) {
+        if (audioDisabled) {
+            return;
+        }
+
+        // can be stream or buffer -> so try to get mediaplayer
+        // if there is non try to stop soundpool
+        MediaPlayer mp = musicPlaying.get(src);
+        if (mp != null) {
+            mp.stop();
+            mp.reset();
+            src.setStatus(Status.Stopped);
+        } else {
+            int channel = src.getChannel();
+            if (channel != -1) {
+                soundPool.pause(channel); // is not very likley to make
+                // something useful :)
+            }
+        }
+
+    }
+
+    @Override
+    public void deleteAudioData(AudioData ad) {
+
+        for (AudioSource src : musicPlaying.keySet()) {
+            if (src.getAudioData() == ad) {
+                MediaPlayer mp = musicPlaying.remove(src);
+                mp.stop();
+                mp.release();
+                src.setStatus(Status.Stopped);
+                src.setChannel(-1);
+                ad.setId(-1);
+                break;
+            }
+        }
+
+        if (ad.getId() > 0) {
+            soundPool.unload(ad.getId());
+            ad.setId(-1);
+        }
+    }
+
+    @Override
+    public void setEnvironment(Environment env) {
+        // not yet supported
+    }
+
+    @Override
+    public void deleteFilter(Filter filter) {
+    }
+}

+ 1423 - 0
jme3-android/src/main/java/com/jme3/audio/android/AndroidOpenALSoftAudioRenderer.java

@@ -0,0 +1,1423 @@
+/*
+ * Copyright (c) 2009-2012 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.audio.android;
+
+import com.jme3.audio.*;
+import com.jme3.audio.AudioSource.Status;
+import com.jme3.math.Vector3f;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.NativeObjectManager;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class AndroidOpenALSoftAudioRenderer implements AndroidAudioRenderer, Runnable {
+
+    private static final Logger logger = Logger.getLogger(AndroidOpenALSoftAudioRenderer.class.getName());
+    private final NativeObjectManager objManager = new NativeObjectManager();
+    // When multiplied by STREAMING_BUFFER_COUNT, will equal 44100 * 2 * 2
+    // which is exactly 1 second of audio.
+    private static final int BUFFER_SIZE = 35280;
+    private static final int STREAMING_BUFFER_COUNT = 5;
+    private final static int MAX_NUM_CHANNELS = 64;
+    private IntBuffer ib = BufferUtils.createIntBuffer(1);
+    private final FloatBuffer fb = BufferUtils.createVector3Buffer(2);
+    private final ByteBuffer nativeBuf = BufferUtils.createByteBuffer(BUFFER_SIZE);
+    private final byte[] arrayBuf = new byte[BUFFER_SIZE];
+    private int[] channels;
+    private AudioSource[] chanSrcs;
+    private int nextChan = 0;
+    private ArrayList<Integer> freeChans = new ArrayList<Integer>();
+    private Listener listener;
+    private boolean audioDisabled = false;
+    private boolean supportEfx = false;
+    private int auxSends = 0;
+    private int reverbFx = -1;
+    private int reverbFxSlot = -1;
+    // Update audio 20 times per second
+    private static final float UPDATE_RATE = 0.05f;
+    private final Thread audioThread = new Thread(this, "jME3 Audio Thread");
+    private final AtomicBoolean threadLock = new AtomicBoolean(false);
+    private boolean initialized = false;
+
+    public AndroidOpenALSoftAudioRenderer() {
+    }
+
+    public void initialize() {
+        if (!audioThread.isAlive()) {
+            audioThread.setDaemon(true);
+            audioThread.setPriority(Thread.NORM_PRIORITY + 1);
+            audioThread.start();
+        } else {
+            throw new IllegalStateException("Initialize already called");
+        }
+    }
+
+    private void checkDead() {
+        if (audioThread.getState() == Thread.State.TERMINATED) {
+            throw new IllegalStateException("Audio thread is terminated");
+        }
+    }
+
+    public void run() {
+        initInThread();
+        synchronized (threadLock) {
+            threadLock.set(true);
+            threadLock.notifyAll();
+        }
+
+        initialized = true;
+
+        long updateRateNanos = (long) (UPDATE_RATE * 1000000000);
+        mainloop:
+        while (true) {
+            long startTime = System.nanoTime();
+
+            if (Thread.interrupted()) {
+                break;
+            }
+
+            synchronized (threadLock) {
+                updateInThread(UPDATE_RATE);
+            }
+
+            long endTime = System.nanoTime();
+            long diffTime = endTime - startTime;
+
+            if (diffTime < updateRateNanos) {
+                long desiredEndTime = startTime + updateRateNanos;
+                while (System.nanoTime() < desiredEndTime) {
+                    try {
+                        Thread.sleep(1);
+                    } catch (InterruptedException ex) {
+                        break mainloop;
+                    }
+                }
+            }
+        }
+
+        initialized = false;
+
+        synchronized (threadLock) {
+            cleanupInThread();
+        }
+    }
+
+    public void initInThread() {
+        try {
+            if (!alIsCreated()) {
+                //AL.create();
+                alCreate();
+                checkError(false);
+            }
+//        } catch (OpenALException ex) {
+//            logger.log(Level.SEVERE, "Failed to load audio library", ex);
+//            audioDisabled = true;
+//            return;
+//        } catch (LWJGLException ex) {
+//            logger.log(Level.SEVERE, "Failed to load audio library", ex);
+//            audioDisabled = true;
+//            return;
+        } catch (UnsatisfiedLinkError ex) {
+            logger.log(Level.SEVERE, "Failed to load audio library", ex);
+            audioDisabled = true;
+            return;
+        }
+
+        //ALCdevice device = AL.getDevice(); /* device maintained in jni */
+        //String deviceName = ALC10.alcGetString(device, ALC10.ALC_DEVICE_SPECIFIER);
+        String deviceName = alcGetString(AL.ALC_DEVICE_SPECIFIER);
+
+        logger.log(Level.INFO, "Audio Device: {0}", deviceName);
+        //logger.log(Level.INFO, "Audio Vendor: {0}", alGetString(AL_VENDOR));
+        //logger.log(Level.INFO, "Audio Renderer: {0}", alGetString(AL_RENDERER));
+        //logger.log(Level.INFO, "Audio Version: {0}", alGetString(AL_VERSION));
+        logger.log(Level.INFO, "Audio Vendor: {0}", alGetString(AL.AL_VENDOR));
+        logger.log(Level.INFO, "Audio Renderer: {0}", alGetString(AL.AL_RENDERER));
+        logger.log(Level.INFO, "Audio Version: {0}", alGetString(AL.AL_VERSION));
+
+        // Find maximum # of sources supported by this implementation
+        ArrayList<Integer> channelList = new ArrayList<Integer>();
+        for (int i = 0; i < MAX_NUM_CHANNELS; i++) {
+            int chan = alGenSources();
+            //if (alGetError() != 0) {
+            if (checkError(false) != 0) {
+                break;
+            } else {
+                channelList.add(chan);
+            }
+        }
+
+        channels = new int[channelList.size()];
+        for (int i = 0; i < channels.length; i++) {
+            channels[i] = channelList.get(i);
+        }
+
+        ib = BufferUtils.createIntBuffer(channels.length);
+        chanSrcs = new AudioSource[channels.length];
+
+        logger.log(Level.INFO, "AudioRenderer supports {0} channels", channels.length);
+
+        //supportEfx = alcIsExtensionPresent(device, "ALC_EXT_EFX");
+        supportEfx = alcIsExtensionPresent(AL.ALC_EXT_EFX_NAME);
+
+        if (supportEfx) {
+            ib.position(0).limit(1);
+            //ALC10.alcGetInteger(device, EFX10.ALC_EFX_MAJOR_VERSION, ib);
+            alcGetInteger(AL.ALC_EFX_MAJOR_VERSION, ib, 1);
+            int major = ib.get(0);
+            ib.position(0).limit(1);
+            //ALC10.alcGetInteger(device, EFX10.ALC_EFX_MINOR_VERSION, ib);
+            alcGetInteger(AL.ALC_EFX_MINOR_VERSION, ib, 1);
+            int minor = ib.get(0);
+            logger.log(Level.INFO, "Audio effect extension version: {0}.{1}", new Object[]{major, minor});
+
+            //ALC10.alcGetInteger(device, EFX10.ALC_MAX_AUXILIARY_SENDS, ib);
+            alcGetInteger(AL.ALC_MAX_AUXILIARY_SENDS, ib, 1);
+            auxSends = ib.get(0);
+            logger.log(Level.INFO, "Audio max auxilary sends: {0}", auxSends);
+
+            // create slot
+            ib.position(0).limit(1);
+            //EFX10.alGenAuxiliaryEffectSlots(ib);
+            alGenAuxiliaryEffectSlots(1, ib);
+            reverbFxSlot = ib.get(0);
+
+            // create effect
+            ib.position(0).limit(1);
+            //EFX10.alGenEffects(ib);
+            alGenEffects(1, ib);
+            reverbFx = ib.get(0);
+            //EFX10.alEffecti(reverbFx, EFX10.AL_EFFECT_TYPE, EFX10.AL_EFFECT_REVERB);
+            alEffecti(reverbFx, AL.AL_EFFECT_TYPE, AL.AL_EFFECT_REVERB);
+
+            // attach reverb effect to effect slot
+            //EFX10.alAuxiliaryEffectSloti(reverbFxSlot, EFX10.AL_EFFECTSLOT_EFFECT, reverbFx);
+            alAuxiliaryEffectSloti(reverbFxSlot, AL.AL_EFFECTSLOT_EFFECT, reverbFx);
+        } else {
+            logger.log(Level.WARNING, "OpenAL EFX not available! Audio effects won't work.");
+        }
+    }
+
+    public void cleanupInThread() {
+        if (audioDisabled) {
+            //AL.destroy();
+            alDestroy();
+            checkError(true);
+            return;
+        }
+
+        // stop any playing channels
+        for (int i = 0; i < chanSrcs.length; i++) {
+            if (chanSrcs[i] != null) {
+                clearChannel(i);
+            }
+        }
+
+        // delete channel-based sources
+        ib.clear();
+        ib.put(channels);
+        ib.flip();
+        //alDeleteSources(ib);
+        alDeleteSources(channels.length, ib);
+        checkError(true);
+
+        // delete audio buffers and filters
+        objManager.deleteAllObjects(this);
+
+        if (supportEfx) {
+            ib.position(0).limit(1);
+            ib.put(0, reverbFx);
+            //EFX10.alDeleteEffects(ib);
+            alDeleteEffects(1, ib);
+
+            // If this is not allocated, why is it deleted?
+            // Commented out to fix native crash in OpenAL.
+            ib.position(0).limit(1);
+            ib.put(0, reverbFxSlot);
+            //EFX10.alDeleteAuxiliaryEffectSlots(ib);
+            alDeleteAuxiliaryEffectSlots(1, ib);
+        }
+
+        //AL.destroy();
+        logger.log(Level.INFO, "Destroying OpenAL Soft Renderer");
+        alDestroy();
+    }
+
+    public void cleanup() {
+        // kill audio thread
+        if (audioThread.isAlive()) {
+            audioThread.interrupt();
+        }
+    }
+
+    private void updateFilter(Filter f) {
+        int id = f.getId();
+        if (id == -1) {
+            ib.position(0).limit(1);
+            //EFX10.alGenFilters(ib);
+            alGenFilters(1, ib);
+            id = ib.get(0);
+            f.setId(id);
+
+            objManager.registerObject(f);
+        }
+
+        if (f instanceof LowPassFilter) {
+            LowPassFilter lpf = (LowPassFilter) f;
+            //EFX10.alFilteri(id, EFX10.AL_FILTER_TYPE, EFX10.AL_FILTER_LOWPASS);
+            alFilteri(id, AL.AL_FILTER_TYPE, AL.AL_FILTER_LOWPASS);
+            //EFX10.alFilterf(id, EFX10.AL_LOWPASS_GAIN, lpf.getVolume());
+            alFilterf(id, AL.AL_LOWPASS_GAIN, lpf.getVolume());
+            //EFX10.alFilterf(id, EFX10.AL_LOWPASS_GAINHF, lpf.getHighFreqVolume());
+            alFilterf(id, AL.AL_LOWPASS_GAINHF, lpf.getHighFreqVolume());
+        } else {
+            throw new UnsupportedOperationException("Filter type unsupported: "
+                    + f.getClass().getName());
+        }
+
+        f.clearUpdateNeeded();
+    }
+
+    public void updateSourceParam(AudioSource src, AudioParam param) {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            // There is a race condition in AudioSource that can
+            // cause this to be called for a node that has been
+            // detached from its channel.  For example, setVolume()
+            // called from the render thread may see that that AudioSource
+            // still has a channel value but the audio thread may
+            // clear that channel before setVolume() gets to call
+            // updateSourceParam() (because the audio stopped playing
+            // on its own right as the volume was set).  In this case,
+            // it should be safe to just ignore the update
+            if (src.getChannel() < 0) {
+                return;
+            }
+
+            assert src.getChannel() >= 0;
+
+            int id = channels[src.getChannel()];
+            switch (param) {
+                case Position:
+                    if (!src.isPositional()) {
+                        return;
+                    }
+
+                    Vector3f pos = src.getPosition();
+                    //alSource3f(id, AL_POSITION, pos.x, pos.y, pos.z);
+                    alSource3f(id, AL.AL_POSITION, pos.x, pos.y, pos.z);
+                    checkError(true);
+                    break;
+                case Velocity:
+                    if (!src.isPositional()) {
+                        return;
+                    }
+
+                    Vector3f vel = src.getVelocity();
+                    //alSource3f(id, AL_VELOCITY, vel.x, vel.y, vel.z);
+                    alSource3f(id, AL.AL_VELOCITY, vel.x, vel.y, vel.z);
+                    checkError(true);
+                    break;
+                case MaxDistance:
+                    if (!src.isPositional()) {
+                        return;
+                    }
+
+                    //alSourcef(id, AL_MAX_DISTANCE, src.getMaxDistance());
+                    alSourcef(id, AL.AL_MAX_DISTANCE, src.getMaxDistance());
+                    checkError(true);
+                    break;
+                case RefDistance:
+                    if (!src.isPositional()) {
+                        return;
+                    }
+
+                    //alSourcef(id, AL_REFERENCE_DISTANCE, src.getRefDistance());
+                    alSourcef(id, AL.AL_REFERENCE_DISTANCE, src.getRefDistance());
+                    checkError(true);
+                    break;
+                case ReverbFilter:
+                    if (!supportEfx || !src.isPositional() || !src.isReverbEnabled()) {
+                        return;
+                    }
+
+                    int filter = AL.AL_FILTER_NULL;
+                    if (src.getReverbFilter() != null) {
+                        Filter f = src.getReverbFilter();
+                        if (f.isUpdateNeeded()) {
+                            updateFilter(f);
+                        }
+                        filter = f.getId();
+                    }
+                    //AL11.alSource3i(id, EFX10.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter);
+                    alSource3i(id, AL.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter);
+                    break;
+                case ReverbEnabled:
+                    if (!supportEfx || !src.isPositional()) {
+                        return;
+                    }
+
+                    if (src.isReverbEnabled()) {
+                        updateSourceParam(src, AudioParam.ReverbFilter);
+                    } else {
+                        //AL11.alSource3i(id, EFX10.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX10.AL_FILTER_NULL);
+                        alSource3i(id, AL.AL_AUXILIARY_SEND_FILTER, 0, 0, AL.AL_FILTER_NULL);
+                    }
+                    break;
+                case IsPositional:
+                    if (!src.isPositional()) {
+                        // Play in headspace
+                        //alSourcei(id, AL_SOURCE_RELATIVE, AL_TRUE);
+                        alSourcei(id, AL.AL_SOURCE_RELATIVE, AL.AL_TRUE);
+                        checkError(true);
+                        //alSource3f(id, AL_POSITION, 0, 0, 0);
+                        alSource3f(id, AL.AL_POSITION, 0, 0, 0);
+                        checkError(true);
+                        //alSource3f(id, AL_VELOCITY, 0, 0, 0);
+                        alSource3f(id, AL.AL_VELOCITY, 0, 0, 0);
+                        checkError(true);
+
+                        // Disable reverb
+                        //AL11.alSource3i(id, EFX10.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX10.AL_FILTER_NULL);
+                        alSource3i(id, AL.AL_AUXILIARY_SEND_FILTER, 0, 0, AL.AL_FILTER_NULL);
+                    } else {
+                        //alSourcei(id, AL_SOURCE_RELATIVE, AL_FALSE);
+                        alSourcei(id, AL.AL_SOURCE_RELATIVE, AL.AL_FALSE);
+                        checkError(true);
+                        updateSourceParam(src, AudioParam.Position);
+                        updateSourceParam(src, AudioParam.Velocity);
+                        updateSourceParam(src, AudioParam.MaxDistance);
+                        updateSourceParam(src, AudioParam.RefDistance);
+                        updateSourceParam(src, AudioParam.ReverbEnabled);
+                    }
+                    break;
+                case Direction:
+                    if (!src.isDirectional()) {
+                        return;
+                    }
+
+                    Vector3f dir = src.getDirection();
+                    //alSource3f(id, AL_DIRECTION, dir.x, dir.y, dir.z);
+                    alSource3f(id, AL.AL_DIRECTION, dir.x, dir.y, dir.z);
+                    checkError(true);
+                    break;
+                case InnerAngle:
+                    if (!src.isDirectional()) {
+                        return;
+                    }
+
+                    //alSourcef(id, AL_CONE_INNER_ANGLE, src.getInnerAngle());
+                    alSourcef(id, AL.AL_CONE_INNER_ANGLE, src.getInnerAngle());
+                    checkError(true);
+                    break;
+                case OuterAngle:
+                    if (!src.isDirectional()) {
+                        return;
+                    }
+
+                    //alSourcef(id, AL_CONE_OUTER_ANGLE, src.getOuterAngle());
+                    alSourcef(id, AL.AL_CONE_OUTER_ANGLE, src.getOuterAngle());
+                    checkError(true);
+                    break;
+                case IsDirectional:
+                    if (src.isDirectional()) {
+                        updateSourceParam(src, AudioParam.Direction);
+                        updateSourceParam(src, AudioParam.InnerAngle);
+                        updateSourceParam(src, AudioParam.OuterAngle);
+                        //alSourcef(id, AL_CONE_OUTER_GAIN, 0);
+                        alSourcef(id, AL.AL_CONE_OUTER_GAIN, 0);
+                        checkError(true);
+                    } else {
+                        //alSourcef(id, AL_CONE_INNER_ANGLE, 360);
+                        alSourcef(id, AL.AL_CONE_INNER_ANGLE, 360);
+                        checkError(true);
+                        //alSourcef(id, AL_CONE_OUTER_ANGLE, 360);
+                        alSourcef(id, AL.AL_CONE_OUTER_ANGLE, 360);
+                        checkError(true);
+                        //alSourcef(id, AL_CONE_OUTER_GAIN, 1f);
+                        alSourcef(id, AL.AL_CONE_OUTER_GAIN, 1f);
+                        checkError(true);
+                    }
+                    break;
+                case DryFilter:
+                    if (!supportEfx) {
+                        return;
+                    }
+
+                    if (src.getDryFilter() != null) {
+                        Filter f = src.getDryFilter();
+                        if (f.isUpdateNeeded()) {
+                            updateFilter(f);
+
+                            // NOTE: must re-attach filter for changes to apply.
+                            //alSourcei(id, EFX10.AL_DIRECT_FILTER, f.getId());
+                            alSourcei(id, AL.AL_DIRECT_FILTER, f.getId());
+                        }
+                    } else {
+                        //alSourcei(id, EFX10.AL_DIRECT_FILTER, EFX10.AL_FILTER_NULL);
+                        alSourcei(id, AL.AL_DIRECT_FILTER, AL.AL_FILTER_NULL);
+                    }
+                    break;
+                case Looping:
+                    if (src.isLooping()) {
+                        if (!(src.getAudioData() instanceof AudioStream)) {
+                            //alSourcei(id, AL_LOOPING, AL_TRUE);
+                            alSourcei(id, AL.AL_LOOPING, AL.AL_TRUE);
+                            checkError(true);
+                        }
+                    } else {
+                        //alSourcei(id, AL_LOOPING, AL_FALSE);
+                        alSourcei(id, AL.AL_LOOPING, AL.AL_FALSE);
+                        checkError(true);
+                    }
+                    break;
+                case Volume:
+                    //alSourcef(id, AL_GAIN, src.getVolume());
+                    alSourcef(id, AL.AL_GAIN, src.getVolume());
+                    checkError(true);
+                    break;
+                case Pitch:
+                    //alSourcef(id, AL_PITCH, src.getPitch());
+                    alSourcef(id, AL.AL_PITCH, src.getPitch());
+                    checkError(true);
+                    break;
+            }
+        }
+    }
+
+    private void setSourceParams(int id, AudioSource src, boolean forceNonLoop) {
+        if (src.isPositional()) {
+            Vector3f pos = src.getPosition();
+            Vector3f vel = src.getVelocity();
+            //alSource3f(id, AL_POSITION, pos.x, pos.y, pos.z);
+            alSource3f(id, AL.AL_POSITION, pos.x, pos.y, pos.z);
+            checkError(true);
+            //alSource3f(id, AL_VELOCITY, vel.x, vel.y, vel.z);
+            alSource3f(id, AL.AL_VELOCITY, vel.x, vel.y, vel.z);
+            checkError(true);
+            //alSourcef(id, AL_MAX_DISTANCE, src.getMaxDistance());
+            alSourcef(id, AL.AL_MAX_DISTANCE, src.getMaxDistance());
+            checkError(true);
+            //alSourcef(id, AL_REFERENCE_DISTANCE, src.getRefDistance());
+            alSourcef(id, AL.AL_REFERENCE_DISTANCE, src.getRefDistance());
+            checkError(true);
+            //alSourcei(id, AL_SOURCE_RELATIVE, AL_FALSE);
+            alSourcei(id, AL.AL_SOURCE_RELATIVE, AL.AL_FALSE);
+            checkError(true);
+
+            if (src.isReverbEnabled() && supportEfx) {
+                //int filter = EFX10.AL_FILTER_NULL;
+                int filter = AL.AL_FILTER_NULL;
+                if (src.getReverbFilter() != null) {
+                    Filter f = src.getReverbFilter();
+                    if (f.isUpdateNeeded()) {
+                        updateFilter(f);
+                    }
+                    filter = f.getId();
+                }
+                //AL11.alSource3i(id, EFX10.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter);
+                alSource3i(id, AL.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter);
+            }
+        } else {
+            // play in headspace
+            //alSourcei(id, AL_SOURCE_RELATIVE, AL_TRUE);
+            alSourcei(id, AL.AL_SOURCE_RELATIVE, AL.AL_TRUE);
+            checkError(true);
+            //alSource3f(id, AL_POSITION, 0, 0, 0);
+            alSource3f(id, AL.AL_POSITION, 0, 0, 0);
+            checkError(true);
+            //alSource3f(id, AL_VELOCITY, 0, 0, 0);
+            alSource3f(id, AL.AL_VELOCITY, 0, 0, 0);
+            checkError(true);
+        }
+
+        if (src.getDryFilter() != null && supportEfx) {
+            Filter f = src.getDryFilter();
+            if (f.isUpdateNeeded()) {
+                updateFilter(f);
+
+                // NOTE: must re-attach filter for changes to apply.
+                //alSourcei(id, EFX10.AL_DIRECT_FILTER, f.getId());
+                alSourcei(id, AL.AL_DIRECT_FILTER, f.getId());
+            }
+        }
+
+        if (forceNonLoop) {
+            //alSourcei(id, AL_LOOPING, AL_FALSE);
+            alSourcei(id, AL.AL_LOOPING, AL.AL_FALSE);
+            checkError(true);
+        } else {
+            //alSourcei(id, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE);
+            alSourcei(id, AL.AL_LOOPING, src.isLooping() ? AL.AL_TRUE : AL.AL_FALSE);
+            checkError(true);
+        }
+        //alSourcef(id, AL_GAIN, src.getVolume());
+        alSourcef(id, AL.AL_GAIN, src.getVolume());
+        checkError(true);
+        //alSourcef(id, AL_PITCH, src.getPitch());
+        alSourcef(id, AL.AL_PITCH, src.getPitch());
+        checkError(true);
+        //alSourcef(id, AL11.AL_SEC_OFFSET, src.getTimeOffset());
+        alSourcef(id, AL.AL_SEC_OFFSET, src.getTimeOffset());
+        checkError(true);
+
+        if (src.isDirectional()) {
+            Vector3f dir = src.getDirection();
+            //alSource3f(id, AL_DIRECTION, dir.x, dir.y, dir.z);
+            alSource3f(id, AL.AL_DIRECTION, dir.x, dir.y, dir.z);
+            checkError(true);
+            //alSourcef(id, AL_CONE_INNER_ANGLE, src.getInnerAngle());
+            alSourcef(id, AL.AL_CONE_INNER_ANGLE, src.getInnerAngle());
+            checkError(true);
+            //alSourcef(id, AL_CONE_OUTER_ANGLE, src.getOuterAngle());
+            alSourcef(id, AL.AL_CONE_OUTER_ANGLE, src.getOuterAngle());
+            checkError(true);
+            //alSourcef(id, AL_CONE_OUTER_GAIN, 0);
+            alSourcef(id, AL.AL_CONE_OUTER_GAIN, 0);
+            checkError(true);
+        } else {
+            //alSourcef(id, AL_CONE_INNER_ANGLE, 360);
+            alSourcef(id, AL.AL_CONE_INNER_ANGLE, 360);
+            checkError(true);
+            //alSourcef(id, AL_CONE_OUTER_ANGLE, 360);
+            alSourcef(id, AL.AL_CONE_OUTER_ANGLE, 360);
+            checkError(true);
+            //alSourcef(id, AL_CONE_OUTER_GAIN, 1f);
+            alSourcef(id, AL.AL_CONE_OUTER_GAIN, 1f);
+            checkError(true);
+        }
+    }
+
+    public void updateListenerParam(Listener listener, ListenerParam param) {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            switch (param) {
+                case Position:
+                    Vector3f pos = listener.getLocation();
+                    //alListener3f(AL_POSITION, pos.x, pos.y, pos.z);
+                    alListener3f(AL.AL_POSITION, pos.x, pos.y, pos.z);
+                    checkError(true);
+                    break;
+                case Rotation:
+                    Vector3f dir = listener.getDirection();
+                    Vector3f up = listener.getUp();
+                    fb.rewind();
+                    fb.put(dir.x).put(dir.y).put(dir.z);
+                    fb.put(up.x).put(up.y).put(up.z);
+                    fb.flip();
+                    //alListener(AL_ORIENTATION, fb);
+                    alListener(AL.AL_ORIENTATION, fb);
+                    checkError(true);
+                    break;
+                case Velocity:
+                    Vector3f vel = listener.getVelocity();
+                    //alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z);
+                    alListener3f(AL.AL_VELOCITY, vel.x, vel.y, vel.z);
+                    checkError(true);
+                    break;
+                case Volume:
+                    //alListenerf(AL_GAIN, listener.getVolume());
+                    alListenerf(AL.AL_GAIN, listener.getVolume());
+                    checkError(true);
+                    break;
+            }
+        }
+    }
+
+    private void setListenerParams(Listener listener) {
+        Vector3f pos = listener.getLocation();
+        Vector3f vel = listener.getVelocity();
+        Vector3f dir = listener.getDirection();
+        Vector3f up = listener.getUp();
+
+        //alListener3f(AL_POSITION, pos.x, pos.y, pos.z);
+        alListener3f(AL.AL_POSITION, pos.x, pos.y, pos.z);
+        checkError(true);
+        //alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z);
+        alListener3f(AL.AL_VELOCITY, vel.x, vel.y, vel.z);
+        checkError(true);
+        fb.rewind();
+        fb.put(dir.x).put(dir.y).put(dir.z);
+        fb.put(up.x).put(up.y).put(up.z);
+        fb.flip();
+        //alListener(AL_ORIENTATION, fb);
+        alListener(AL.AL_ORIENTATION, fb);
+        checkError(true);
+        //alListenerf(AL_GAIN, listener.getVolume());
+        alListenerf(AL.AL_GAIN, listener.getVolume());
+        checkError(true);
+    }
+
+    private int newChannel() {
+        if (freeChans.size() > 0) {
+            return freeChans.remove(0);
+        } else if (nextChan < channels.length) {
+            return nextChan++;
+        } else {
+            return -1;
+        }
+    }
+
+    private void freeChannel(int index) {
+        if (index == nextChan - 1) {
+            nextChan--;
+        } else {
+            freeChans.add(index);
+        }
+    }
+
+    public void setEnvironment(Environment env) {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled || !supportEfx) {
+                return;
+            }
+
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_DENSITY, env.getDensity());
+            alEffectf(reverbFx, AL.AL_REVERB_DENSITY, env.getDensity());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_DIFFUSION, env.getDiffusion());
+            alEffectf(reverbFx, AL.AL_REVERB_DIFFUSION, env.getDiffusion());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_GAIN, env.getGain());
+            alEffectf(reverbFx, AL.AL_REVERB_GAIN, env.getGain());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_GAINHF, env.getGainHf());
+            alEffectf(reverbFx, AL.AL_REVERB_GAINHF, env.getGainHf());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_DECAY_TIME, env.getDecayTime());
+            alEffectf(reverbFx, AL.AL_REVERB_DECAY_TIME, env.getDecayTime());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_DECAY_HFRATIO, env.getDecayHFRatio());
+            alEffectf(reverbFx, AL.AL_REVERB_DECAY_HFRATIO, env.getDecayHFRatio());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_REFLECTIONS_GAIN, env.getReflectGain());
+            alEffectf(reverbFx, AL.AL_REVERB_REFLECTIONS_GAIN, env.getReflectGain());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_REFLECTIONS_DELAY, env.getReflectDelay());
+            alEffectf(reverbFx, AL.AL_REVERB_REFLECTIONS_DELAY, env.getReflectDelay());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_LATE_REVERB_GAIN, env.getLateReverbGain());
+            alEffectf(reverbFx, AL.AL_REVERB_LATE_REVERB_GAIN, env.getLateReverbGain());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_LATE_REVERB_DELAY, env.getLateReverbDelay());
+            alEffectf(reverbFx, AL.AL_REVERB_LATE_REVERB_DELAY, env.getLateReverbDelay());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_AIR_ABSORPTION_GAINHF, env.getAirAbsorbGainHf());
+            alEffectf(reverbFx, AL.AL_REVERB_AIR_ABSORPTION_GAINHF, env.getAirAbsorbGainHf());
+            //EFX10.alEffectf(reverbFx, EFX10.AL_REVERB_ROOM_ROLLOFF_FACTOR, env.getRoomRolloffFactor());
+            alEffectf(reverbFx, AL.AL_REVERB_ROOM_ROLLOFF_FACTOR, env.getRoomRolloffFactor());
+
+            // attach effect to slot
+            //EFX10.alAuxiliaryEffectSloti(reverbFxSlot, EFX10.AL_EFFECTSLOT_EFFECT, reverbFx);
+            alAuxiliaryEffectSloti(reverbFxSlot, AL.AL_EFFECTSLOT_EFFECT, reverbFx);
+        }
+    }
+
+    private boolean fillBuffer(AudioStream stream, int id) {
+        int size = 0;
+        int result;
+
+        while (size < arrayBuf.length) {
+            result = stream.readSamples(arrayBuf, size, arrayBuf.length - size);
+
+            if (result > 0) {
+                size += result;
+            } else {
+                break;
+            }
+        }
+
+        if (size == 0) {
+            return false;
+        }
+
+        nativeBuf.clear();
+        nativeBuf.put(arrayBuf, 0, size);
+        nativeBuf.flip();
+
+        //alBufferData(id, convertFormat(stream), nativeBuf, stream.getSampleRate());
+        alBufferData(id, convertFormat(stream), nativeBuf, size, stream.getSampleRate());
+        checkError(true);
+
+        return true;
+    }
+
+    private boolean fillStreamingSource(int sourceId, AudioStream stream) {
+        if (!stream.isOpen()) {
+            return false;
+        }
+
+        boolean active = true;
+        //int processed = alGetSourcei(sourceId, AL_BUFFERS_PROCESSED);
+        int processed = alGetSourcei(sourceId, AL.AL_BUFFERS_PROCESSED);
+        checkError(true);
+
+        //while((processed--) != 0){
+        if (processed > 0) {
+            int buffer;
+
+            ib.position(0).limit(1);
+            //alSourceUnqueueBuffers(sourceId, ib);
+            alSourceUnqueueBuffers(sourceId, 1, ib);
+            checkError(true);
+            buffer = ib.get(0);
+
+            active = fillBuffer(stream, buffer);
+
+            ib.position(0).limit(1);
+            ib.put(0, buffer);
+            //alSourceQueueBuffers(sourceId, ib);
+            alSourceQueueBuffers(sourceId, 1, ib);
+            checkError(true);
+        }
+
+        if (!active && stream.isOpen()) {
+            stream.close();
+        }
+
+        return active;
+    }
+
+    private boolean attachStreamToSource(int sourceId, AudioStream stream) {
+        boolean active = true;
+        int activeBufferCount = 0;
+        for (int id : stream.getIds()) {
+            active = fillBuffer(stream, id);
+            ib.position(0).limit(1);
+            ib.put(id).flip();
+            //alSourceQueueBuffers(sourceId, ib);
+            // OpenAL Soft does not like 0 size buffer data in alSourceQueueBuffers
+            //  Produces error code 40964 (0xA004) = AL_INVALID_OPERATION and
+            //  does not return (crashes) so that the error code can be checked.
+            // active is FALSE when the data size is 0
+            if (active) {
+                alSourceQueueBuffers(sourceId, 1, ib);
+                checkError(true);
+                activeBufferCount++;
+            }
+        }
+        // adjust the steam id array if the audio data is smaller than STREAMING_BUFFER_COUNT
+        // this is to avoid an error with OpenAL Soft when alSourceUnenqueueBuffers
+        //   is called with more buffers than were originally used with alSourceQueueBuffers
+        if (activeBufferCount < STREAMING_BUFFER_COUNT) {
+            int[] newIds = new int[activeBufferCount];
+            for (int i=0; i<STREAMING_BUFFER_COUNT; i++) {
+                if (i < activeBufferCount) {
+                    newIds[i] = stream.getIds()[i];
+                } else {
+                    ib.clear();
+                    ib.put(stream.getIds()[i]).limit(1).flip();
+                    alDeleteBuffers(1, ib);
+                    checkError(true);
+                }
+
+            }
+            stream.setIds(newIds);
+        }
+
+        return active;
+    }
+
+    private boolean attachBufferToSource(int sourceId, AudioBuffer buffer) {
+        //alSourcei(sourceId, AL_BUFFER, buffer.getId());
+        alSourcei(sourceId, AL.AL_BUFFER, buffer.getId());
+        checkError(true);
+        return true;
+    }
+
+    private boolean attachAudioToSource(int sourceId, AudioData data) {
+        if (data instanceof AudioBuffer) {
+            return attachBufferToSource(sourceId, (AudioBuffer) data);
+        } else if (data instanceof AudioStream) {
+            return attachStreamToSource(sourceId, (AudioStream) data);
+        }
+        throw new UnsupportedOperationException();
+    }
+
+    private void clearChannel(int index) {
+        // make room at this channel
+        if (chanSrcs[index] != null) {
+            AudioSource src = chanSrcs[index];
+
+            int sourceId = channels[index];
+            alSourceStop(sourceId);
+
+            if (src.getAudioData() instanceof AudioStream) {
+                AudioStream str = (AudioStream) src.getAudioData();
+                for (int i=0; i<str.getIds().length; i++) {
+                }
+                //ib.position(0).limit(STREAMING_BUFFER_COUNT);
+                ib.position(0).limit(str.getIds().length);
+                ib.put(str.getIds()).flip();
+                int processed = alGetSourcei(sourceId, AL.AL_BUFFERS_PROCESSED);
+                //alSourceUnqueueBuffers(sourceId, ib);
+                alSourceUnqueueBuffers(sourceId, processed, ib);
+                checkError(true);
+            } else if (src.getAudioData() instanceof AudioBuffer) {
+                //alSourcei(sourceId, AL_BUFFER, 0);
+                alSourcei(sourceId, AL.AL_BUFFER, 0);
+                checkError(true);
+            }
+
+            if (src.getDryFilter() != null && supportEfx) {
+                // detach filter
+                //alSourcei(sourceId, EFX10.AL_DIRECT_FILTER, EFX10.AL_FILTER_NULL);
+                alSourcei(sourceId, AL.AL_DIRECT_FILTER, AL.AL_FILTER_NULL);
+            }
+            if (src.isPositional()) {
+                AudioSource pas = (AudioSource) src;
+                if (pas.isReverbEnabled() && supportEfx) {
+                    //AL11.alSource3i(sourceId, EFX10.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX10.AL_FILTER_NULL);
+                    alSource3i(sourceId, AL.AL_AUXILIARY_SEND_FILTER, 0, 0, AL.AL_FILTER_NULL);
+                }
+            }
+
+            chanSrcs[index] = null;
+        }
+    }
+
+    public void update(float tpf) {
+        // does nothing
+    }
+
+    public void updateInThread(float tpf) {
+        if (audioDisabled) {
+            return;
+        }
+
+        for (int i = 0; i < channels.length; i++) {
+            AudioSource src = chanSrcs[i];
+            if (src == null) {
+                continue;
+            }
+
+            int sourceId = channels[i];
+
+            // is the source bound to this channel
+            // if false, it's an instanced playback
+            boolean boundSource = i == src.getChannel();
+
+            // source's data is streaming
+            boolean streaming = src.getAudioData() instanceof AudioStream;
+
+            // only buffered sources can be bound
+            assert (boundSource && streaming) || (!streaming);
+
+            //int state = alGetSourcei(sourceId, AL_SOURCE_STATE);
+            int state = alGetSourcei(sourceId, AL.AL_SOURCE_STATE);
+            checkError(true);
+            boolean wantPlaying = src.getStatus() == Status.Playing;
+            //boolean stopped = state == AL_STOPPED;
+            boolean stopped = state == AL.AL_STOPPED;
+
+            if (streaming && wantPlaying) {
+                AudioStream stream = (AudioStream) src.getAudioData();
+                if (stream.isOpen()) {
+                    fillStreamingSource(sourceId, stream);
+                    if (stopped) {
+                        alSourcePlay(sourceId);
+                        checkError(true);
+                    }
+                } else {
+                    if (stopped) {
+                        // became inactive
+                        src.setStatus(Status.Stopped);
+                        src.setChannel(-1);
+                        clearChannel(i);
+                        freeChannel(i);
+
+                        // And free the audio since it cannot be
+                        // played again anyway.
+                        deleteAudioData(stream);
+                    }
+                }
+            } else if (!streaming) {
+                //boolean paused = state == AL_PAUSED;
+                boolean paused = state == AL.AL_PAUSED;
+
+                // make sure OAL pause state & source state coincide
+                assert (src.getStatus() == Status.Paused && paused) || (!paused);
+
+                if (stopped) {
+                    if (boundSource) {
+                        src.setStatus(Status.Stopped);
+                        src.setChannel(-1);
+                    }
+                    clearChannel(i);
+                    freeChannel(i);
+                }
+            }
+        }
+
+        // Delete any unused objects.
+        objManager.deleteUnused(this);
+    }
+
+    public void setListener(Listener listener) {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            if (this.listener != null) {
+                // previous listener no longer associated with current
+                // renderer
+                this.listener.setRenderer(null);
+            }
+
+            this.listener = listener;
+            this.listener.setRenderer(this);
+            setListenerParams(listener);
+        }
+    }
+
+    public void playSourceInstance(AudioSource src) {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            if (src.getAudioData() instanceof AudioStream) {
+                throw new UnsupportedOperationException(
+                        "Cannot play instances "
+                        + "of audio streams. Use playSource() instead.");
+            }
+
+            if (src.getAudioData().isUpdateNeeded()) {
+                updateAudioData(src.getAudioData());
+            }
+
+            // create a new index for an audio-channel
+            int index = newChannel();
+            if (index == -1) {
+                return;
+            }
+
+            int sourceId = channels[index];
+
+            clearChannel(index);
+
+            // set parameters, like position and max distance
+            setSourceParams(sourceId, src, true);
+            attachAudioToSource(sourceId, src.getAudioData());
+            chanSrcs[index] = src;
+
+            // play the channel
+            alSourcePlay(sourceId);
+            checkError(true);
+        }
+    }
+
+    public void playSource(AudioSource src) {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            //assert src.getStatus() == Status.Stopped || src.getChannel() == -1;
+
+            if (src.getStatus() == Status.Playing) {
+                return;
+            } else if (src.getStatus() == Status.Stopped) {
+
+                // allocate channel to this source
+                int index = newChannel();
+                if (index == -1) {
+                    logger.log(Level.WARNING, "No channel available to play {0}", src);
+                    return;
+                }
+                clearChannel(index);
+                src.setChannel(index);
+
+                AudioData data = src.getAudioData();
+                if (data.isUpdateNeeded()) {
+                    updateAudioData(data);
+                }
+
+                chanSrcs[index] = src;
+                setSourceParams(channels[index], src, false);
+                attachAudioToSource(channels[index], data);
+            }
+
+            alSourcePlay(channels[src.getChannel()]);
+            checkError(true);
+            src.setStatus(Status.Playing);
+        }
+    }
+
+    public void pauseSource(AudioSource src) {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            if (src.getStatus() == Status.Playing) {
+                assert src.getChannel() != -1;
+
+                alSourcePause(channels[src.getChannel()]);
+                checkError(true);
+                src.setStatus(Status.Paused);
+            }
+        }
+    }
+
+    public void stopSource(AudioSource src) {
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            if (src.getStatus() != Status.Stopped) {
+                int chan = src.getChannel();
+                assert chan != -1; // if it's not stopped, must have id
+
+                src.setStatus(Status.Stopped);
+                src.setChannel(-1);
+                clearChannel(chan);
+                freeChannel(chan);
+
+                if (src.getAudioData() instanceof AudioStream) {
+                    AudioStream stream = (AudioStream) src.getAudioData();
+                    if (stream.isOpen()) {
+                        stream.close();
+                    }
+
+                    // And free the audio since it cannot be
+                    // played again anyway.
+                    deleteAudioData(src.getAudioData());
+                }
+            }
+        }
+    }
+
+    private int convertFormat(AudioData ad) {
+        switch (ad.getBitsPerSample()) {
+            case 8:
+                if (ad.getChannels() == 1) {
+                    //return AL_FORMAT_MONO8;
+                    return AL.AL_FORMAT_MONO8;
+                } else if (ad.getChannels() == 2) {
+                    //return AL_FORMAT_STEREO8;
+                    return AL.AL_FORMAT_STEREO8;
+                }
+
+                break;
+            case 16:
+                if (ad.getChannels() == 1) {
+                    //return AL_FORMAT_MONO16;
+                    return AL.AL_FORMAT_MONO16;
+                } else {
+                    //return AL_FORMAT_STEREO16;
+                    return AL.AL_FORMAT_STEREO16;
+                }
+        }
+        throw new UnsupportedOperationException("Unsupported channels/bits combination: "
+                + "bits=" + ad.getBitsPerSample() + ", channels=" + ad.getChannels());
+    }
+
+    private void updateAudioBuffer(AudioBuffer ab) {
+        int id = ab.getId();
+        if (ab.getId() == -1) {
+            ib.position(0).limit(1);
+            alGenBuffers(1, ib);
+            checkError(true);
+            id = ib.get(0);
+            ab.setId(id);
+
+            objManager.registerObject(ab);
+        }
+
+        ab.getData().clear();
+        //alBufferData(id, convertFormat(ab), ab.getData(), ab.getSampleRate());
+        alBufferData(id, convertFormat(ab), ab.getData(), ab.getData().limit(), ab.getSampleRate());
+        checkError(true);
+        ab.clearUpdateNeeded();
+    }
+
+    private void updateAudioStream(AudioStream as) {
+        if (as.getIds() != null) {
+            deleteAudioData(as);
+        }
+
+        int[] ids = new int[STREAMING_BUFFER_COUNT];
+        ib.position(0).limit(STREAMING_BUFFER_COUNT);
+        //alGenBuffers(ib);
+        alGenBuffers(STREAMING_BUFFER_COUNT, ib);
+        checkError(true);
+        ib.position(0).limit(STREAMING_BUFFER_COUNT);
+        ib.get(ids);
+
+        // Not registered with object manager.
+        // AudioStreams can be handled without object manager
+        // since their lifecycle is known to the audio renderer.
+
+        as.setIds(ids);
+        as.clearUpdateNeeded();
+    }
+
+    private void updateAudioData(AudioData ad) {
+        if (ad instanceof AudioBuffer) {
+            updateAudioBuffer((AudioBuffer) ad);
+        } else if (ad instanceof AudioStream) {
+            updateAudioStream((AudioStream) ad);
+        }
+    }
+
+    public void deleteFilter(Filter filter) {
+        int id = filter.getId();
+        if (id != -1) {
+            //EFX10.alDeleteFilters(id);
+            ib.put(0, id);
+            ib.position(0).limit(1);
+            alDeleteFilters(1, ib);
+        }
+    }
+
+    public void deleteAudioData(AudioData ad) {
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            if (ad instanceof AudioBuffer) {
+                AudioBuffer ab = (AudioBuffer) ad;
+                int id = ab.getId();
+                if (id != -1) {
+                    ib.put(0, id);
+                    ib.position(0).limit(1);
+                    //alDeleteBuffers(ib);
+                    alDeleteBuffers(1, ib);
+                    checkError(true);
+                    ab.resetObject();
+                }
+            } else if (ad instanceof AudioStream) {
+                AudioStream as = (AudioStream) ad;
+                int[] ids = as.getIds();
+                if (ids != null) {
+                    ib.clear();
+                    ib.put(ids).flip();
+                    //alDeleteBuffers(ib);
+                    alDeleteBuffers(ids.length, ib);
+                    checkError(true);
+                    as.resetObject();
+                }
+            }
+        }
+    }
+
+    public void pauseAll() {
+        // don't try to pause all audio (mainly from Android activity) if
+        //   the renderer is already closed down
+        if (!initialized) {
+            return;
+        }
+
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            for (int i = 0; i < channels.length; i++) {
+                AudioSource src = chanSrcs[i];
+                if (src == null) {
+                    continue;
+                }
+
+                if (src.getStatus() == Status.Playing) {
+                    assert src.getChannel() != -1;
+
+                    logger.log(Level.FINE, "Pausing Source: {0}", src.getChannel());
+                    alSourcePause(channels[src.getChannel()]);
+                    checkError(true);
+                    src.setStatus(Status.Paused);
+                }
+            }
+
+        }
+    }
+
+    public void resumeAll() {
+        checkDead();
+        synchronized (threadLock) {
+            while (!threadLock.get()) {
+                try {
+                    threadLock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+            if (audioDisabled) {
+                return;
+            }
+
+            for (int i = 0; i < channels.length; i++) {
+                AudioSource src = chanSrcs[i];
+                if (src == null) {
+                    continue;
+                }
+
+                if (src.getStatus() == Status.Paused) {
+                    assert src.getChannel() != -1;
+
+                    logger.log(Level.FINE, "Playing/Resuming Source: {0}", src.getChannel());
+                    alSourcePlay(channels[src.getChannel()]);
+                    checkError(true);
+                    src.setStatus(Status.Playing);
+                }
+            }
+        }
+    }
+
+    private int checkError(boolean stopOnError) {
+        int errorCode = alGetError();
+        String errorText = AL.GetALErrorMsg(errorCode);
+
+        if (errorCode != AL.AL_NO_ERROR && stopOnError) {
+            throw new IllegalStateException("AL Error Detected.  Error Code: " + errorCode + ": " + errorText);
+        }
+
+        return errorCode;
+    }
+
+    /** Native methods, implemented in jni folder */
+    public static native boolean alIsCreated();
+    public static native boolean alCreate();
+    public static native boolean alDestroy();
+    public static native String alcGetString(int parameter);
+    public static native String alGetString(int parameter);
+    public static native int alGenSources();
+    public static native int alGetError();
+    public static native void alDeleteSources(int numSources, IntBuffer sources);
+    public static native void alGenBuffers(int numBuffers, IntBuffer buffers);
+    public static native void alDeleteBuffers(int numBuffers, IntBuffer buffers);
+    public static native void alSourceStop(int source);
+    public static native void alSourcei(int source, int param, int value);
+    public static native void alBufferData(int buffer, int format, ByteBuffer data, int size, int frequency);
+    public static native void alSourcePlay(int source);
+    public static native void alSourcePause(int source);
+    public static native void alSourcef(int source, int param, float value);
+    public static native void alSource3f(int source, int param, float value1, float value2, float value3);
+    public static native int alGetSourcei(int source, int param);
+    public static native void alSourceUnqueueBuffers(int source, int numBuffers, IntBuffer buffers);
+    public static native void alSourceQueueBuffers(int source, int numBuffers, IntBuffer buffers);
+    public static native void alListener(int param, FloatBuffer data);
+    public static native void alListenerf(int param, float value);
+    public static native void alListener3f(int param, float value1, float value2, float value3);
+    public static native boolean alcIsExtensionPresent(String extension);
+    public static native void alcGetInteger(int param, IntBuffer buffer, int size);
+    public static native void alGenAuxiliaryEffectSlots(int numSlots, IntBuffer buffers);
+    public static native void alGenEffects(int numEffects, IntBuffer buffers);
+    public static native void alEffecti(int effect, int param, int value);
+    public static native void alAuxiliaryEffectSloti(int effectSlot, int param, int value);
+    public static native void alDeleteEffects(int numEffects, IntBuffer buffers);
+    public static native void alDeleteAuxiliaryEffectSlots(int numEffectSlots, IntBuffer buffers);
+    public static native void alGenFilters(int numFilters, IntBuffer buffers);
+    public static native void alFilteri(int filter, int param, int value);
+    public static native void alFilterf(int filter, int param, float value);
+    public static native void alSource3i(int source, int param, int value1, int value2, int value3);
+    public static native void alDeleteFilters(int numFilters, IntBuffer buffers);
+    public static native void alEffectf(int effect, int param, float value);
+
+    /** Load jni .so on initialization */
+    static {
+         System.loadLibrary("openalsoftjme");
+    }
+
+}

+ 20 - 0
jme3-android/src/main/java/com/jme3/audio/plugins/AndroidAudioLoader.java

@@ -0,0 +1,20 @@
+package com.jme3.audio.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.audio.android.AndroidAudioData;
+import java.io.IOException;
+
+/**
+ * <code>AndroidAudioLoader</code> will create an 
+ * {@link AndroidAudioData} object with the specified asset key.
+ */
+public class AndroidAudioLoader implements AssetLoader {
+
+    @Override
+    public Object load(AssetInfo assetInfo) throws IOException {
+        AndroidAudioData result = new AndroidAudioData();
+        result.setAssetKey(assetInfo.getKey());
+        return result;
+    }
+}

+ 348 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidGestureHandler.java

@@ -0,0 +1,348 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import com.jme3.input.event.InputEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * AndroidGestureHandler uses Gesture type listeners to create jME TouchEvents
+ * for gestures.  This class is designed to handle the gestures supported 
+ * on Android rev 9 (Android 2.3).  Extend this class to add functionality
+ * added by Android after rev 9.
+ * 
+ * @author iwgeric
+ */
+public class AndroidGestureHandler implements 
+        GestureDetector.OnGestureListener, 
+        GestureDetector.OnDoubleTapListener,
+        ScaleGestureDetector.OnScaleGestureListener {
+    private static final Logger logger = Logger.getLogger(AndroidGestureHandler.class.getName());
+    private AndroidInputHandler androidInput;
+    private GestureDetector gestureDetector;
+    private ScaleGestureDetector scaleDetector;
+    float gestureDownX = -1f;
+    float gestureDownY = -1f;
+    float scaleStartX = -1f;
+    float scaleStartY = -1f;
+
+    public AndroidGestureHandler(AndroidInputHandler androidInput) {
+        this.androidInput = androidInput;
+    }
+    
+    public void initialize() {
+    }
+    
+    public void destroy() {
+        setView(null);
+    }
+    
+    public void setView(View view) {
+        if (view != null) {
+            gestureDetector = new GestureDetector(view.getContext(), this);
+            scaleDetector = new ScaleGestureDetector(view.getContext(), this);
+        } else {
+            gestureDetector = null;
+            scaleDetector = null;
+        }
+    }
+    
+    public void detectGesture(MotionEvent event) {
+        if (gestureDetector != null && scaleDetector != null) {
+            gestureDetector.onTouchEvent(event);
+            scaleDetector.onTouchEvent(event);
+        }
+    }
+
+    private int getPointerIndex(MotionEvent event) {
+        return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+    }
+    
+    private int getPointerId(MotionEvent event) {
+        return event.getPointerId(getPointerIndex(event));
+    }
+    
+    private void processEvent(TouchEvent event) {
+        // Add the touch event
+        androidInput.addEvent(event);
+        if (androidInput.isSimulateMouse()) {
+            InputEvent mouseEvent = generateMouseEvent(event);
+            if (mouseEvent != null) {
+                // Add the mouse event
+                androidInput.addEvent(mouseEvent);
+            }
+        }
+    }
+
+    // TODO: Ring Buffer for mouse events?
+    private InputEvent generateMouseEvent(TouchEvent event) {
+        InputEvent inputEvent = null;
+        int newX;
+        int newY;
+        int newDX;
+        int newDY;
+
+        if (androidInput.isMouseEventsInvertX()) {
+            newX = (int) (androidInput.invertX(event.getX()));
+            newDX = (int)event.getDeltaX() * -1;
+        } else {
+            newX = (int) event.getX();
+            newDX = (int)event.getDeltaX();
+        }
+        int wheel = (int) (event.getScaleSpan()); // might need to scale to match mouse wheel
+        int dWheel = (int) (event.getDeltaScaleSpan()); // might need to scale to match mouse wheel
+
+        if (androidInput.isMouseEventsInvertY()) {
+            newY = (int) (androidInput.invertY(event.getY()));
+            newDY = (int)event.getDeltaY() * -1;
+        } else {
+            newY = (int) event.getY();
+            newDY = (int)event.getDeltaY();
+        }
+
+        switch (event.getType()) {
+            case SCALE_MOVE:
+                inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, wheel, dWheel);
+                inputEvent.setTime(event.getTime());
+                break;
+        }
+
+        return inputEvent;
+    }
+    
+    /* Events from onGestureListener */
+    
+    public boolean onDown(MotionEvent event) {
+        // start of all GestureListeners.  Not really a gesture by itself
+        // so we don't create an event.
+        // However, reset the scaleInProgress here since this is the beginning
+        // of a series of gesture events.
+//        logger.log(Level.INFO, "onDown pointerId: {0}, action: {1}, x: {2}, y: {3}", 
+//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+        gestureDownX = androidInput.getJmeX(event.getX());
+        gestureDownY = androidInput.invertY(androidInput.getJmeY(event.getY()));
+        return true;
+    }
+
+    public boolean onSingleTapUp(MotionEvent event) {
+        // Up of single tap.  May be followed by a double tap later.
+        // use onSingleTapConfirmed instead.
+//        logger.log(Level.INFO, "onSingleTapUp pointerId: {0}, action: {1}, x: {2}, y: {3}", 
+//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+        return true;
+    }
+
+    public void onShowPress(MotionEvent event) {
+//        logger.log(Level.INFO, "onShowPress pointerId: {0}, action: {1}, x: {2}, y: {3}", 
+//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+        float jmeX = androidInput.getJmeX(event.getX());
+        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.SHOWPRESS, jmeX, jmeY, 0, 0);
+        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setTime(event.getEventTime());
+        touchEvent.setPressure(event.getPressure());
+        processEvent(touchEvent);
+    }
+
+    public void onLongPress(MotionEvent event) {
+//        logger.log(Level.INFO, "onLongPress pointerId: {0}, action: {1}, x: {2}, y: {3}", 
+//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+        float jmeX = androidInput.getJmeX(event.getX());
+        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.LONGPRESSED, jmeX, jmeY, 0, 0);
+        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setTime(event.getEventTime());
+        touchEvent.setPressure(event.getPressure());
+        processEvent(touchEvent);
+    }
+
+    public boolean onScroll(MotionEvent startEvent, MotionEvent endEvent, float distX, float distY) {
+        // if not scaleInProgess, send scroll events.  This is to avoid sending
+        // scroll events when one of the fingers is lifted just before the other one.
+        // Avoids sending the scroll for that brief period of time.
+        // Return true so that the next event doesn't accumulate the distX and distY values.
+        // Apparantly, both distX and distY are negative.  
+        // Negate distX to get the real value, but leave distY negative to compensate
+        // for the fact that jME has y=0 at bottom where Android has y=0 at top.
+//        if (!scaleInProgress) {
+        if (!scaleDetector.isInProgress()) {
+//            logger.log(Level.INFO, "onScroll pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, dx: {7}, dy: {8}", 
+//                    new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), distX, distY});
+
+            float jmeX = androidInput.getJmeX(endEvent.getX());
+            float jmeY = androidInput.invertY(androidInput.getJmeY(endEvent.getY()));
+            TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+            touchEvent.set(TouchEvent.Type.SCROLL, jmeX, jmeY, androidInput.getJmeX(-distX), androidInput.getJmeY(distY));
+            touchEvent.setPointerId(getPointerId(endEvent));
+            touchEvent.setTime(endEvent.getEventTime());
+            touchEvent.setPressure(endEvent.getPressure());
+            processEvent(touchEvent);
+        }
+        return true;
+    }
+
+    public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float velocityX, float velocityY) {
+        // Fling happens only once at the end of the gesture (all fingers up).
+        // Fling returns the velocity of the finger movement in pixels/sec.
+        // Therefore, the dX and dY values are actually velocity instead of distance values
+        // Since this does not track the movement, use the start position and velocity values.
+        
+//        logger.log(Level.INFO, "onFling pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, velocityX: {7}, velocityY: {8}", 
+//                new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), velocityX, velocityY});
+
+        float jmeX = androidInput.getJmeX(startEvent.getX());
+        float jmeY = androidInput.invertY(androidInput.getJmeY(startEvent.getY()));
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.FLING, jmeX, jmeY, velocityX, velocityY);
+        touchEvent.setPointerId(getPointerId(endEvent));
+        touchEvent.setTime(endEvent.getEventTime());
+        touchEvent.setPressure(endEvent.getPressure());
+        processEvent(touchEvent);
+        return true;
+    }
+
+    /* Events from onDoubleTapListener */
+    
+    public boolean onSingleTapConfirmed(MotionEvent event) {
+        // Up of single tap when no double tap followed.
+//        logger.log(Level.INFO, "onSingleTapConfirmed pointerId: {0}, action: {1}, x: {2}, y: {3}", 
+//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+        float jmeX = androidInput.getJmeX(event.getX());
+        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.TAP, jmeX, jmeY, 0, 0);
+        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setTime(event.getEventTime());
+        touchEvent.setPressure(event.getPressure());
+        processEvent(touchEvent);
+        return true;
+    }
+
+    public boolean onDoubleTap(MotionEvent event) {
+        //The down motion event of the first tap of the double-tap
+        // We could use this event to fire off a double tap event, or use 
+        // DoubleTapEvent with a check for the UP action
+//        logger.log(Level.INFO, "onDoubleTap pointerId: {0}, action: {1}, x: {2}, y: {3}", 
+//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+        float jmeX = androidInput.getJmeX(event.getX());
+        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.DOUBLETAP, jmeX, jmeY, 0, 0);
+        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setTime(event.getEventTime());
+        touchEvent.setPressure(event.getPressure());
+        processEvent(touchEvent);
+        return true;
+    }
+
+    public boolean onDoubleTapEvent(MotionEvent event) {
+        //Notified when an event within a double-tap gesture occurs, including the down, move(s), and up events.
+        // this means it will get called multiple times for a single double tap
+//        logger.log(Level.INFO, "onDoubleTapEvent pointerId: {0}, action: {1}, x: {2}, y: {3}", 
+//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+//        if (getAction(event) == MotionEvent.ACTION_UP) {
+//            TouchEvent touchEvent = touchEventPool.getNextFreeEvent();
+//            touchEvent.set(TouchEvent.Type.DOUBLETAP, event.getX(), androidInput.invertY(event.getY()), 0, 0);
+//            touchEvent.setPointerId(getPointerId(event));
+//            touchEvent.setTime(event.getEventTime());
+//            touchEvent.setPressure(event.getPressure());
+//            processEvent(touchEvent);
+//        }
+        return true;
+    }
+
+    /* Events from ScaleGestureDetector */
+    
+    public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
+        // Scale uses a focusX and focusY instead of x and y.  Focus is the middle
+        // of the fingers.  Therefore, use the x and y values from the Down event
+        // so that the x and y values don't jump to the middle position.
+        // return true or all gestures for this beginning event will be discarded
+        logger.log(Level.INFO, "onScaleBegin");
+        scaleStartX = gestureDownX;
+        scaleStartY = gestureDownY;
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.SCALE_START, scaleStartX, scaleStartY, 0f, 0f);
+        touchEvent.setPointerId(0);
+        touchEvent.setTime(scaleGestureDetector.getEventTime());
+        touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
+        touchEvent.setDeltaScaleSpan(0f);
+        touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
+        touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
+        processEvent(touchEvent);
+        
+        return true;
+    }
+
+    public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
+        // return true or all gestures for this event will be accumulated
+        logger.log(Level.INFO, "onScale");
+        scaleStartX = gestureDownX;
+        scaleStartY = gestureDownY;
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.SCALE_MOVE, scaleStartX, scaleStartY, 0f, 0f);
+        touchEvent.setPointerId(0);
+        touchEvent.setTime(scaleGestureDetector.getEventTime());
+        touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
+        touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
+        touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
+        touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
+        processEvent(touchEvent);
+        return true;
+    }
+
+    public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
+        logger.log(Level.INFO, "onScaleEnd");
+        scaleStartX = gestureDownX;
+        scaleStartY = gestureDownY;
+        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        touchEvent.set(TouchEvent.Type.SCALE_END, scaleStartX, scaleStartY, 0f, 0f);
+        touchEvent.setPointerId(0);
+        touchEvent.setTime(scaleGestureDetector.getEventTime());
+        touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
+        touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
+        touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
+        touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
+        processEvent(touchEvent);
+    }
+}

+ 686 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidInput.java

@@ -0,0 +1,686 @@
+package com.jme3.input.android;
+
+import android.view.*;
+import com.jme3.input.KeyInput;
+import com.jme3.input.RawInputListener;
+import com.jme3.input.TouchInput;
+import com.jme3.input.event.MouseButtonEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+import com.jme3.input.event.TouchEvent.Type;
+import com.jme3.math.Vector2f;
+import com.jme3.system.AppSettings;
+import com.jme3.util.RingBuffer;
+import java.util.HashMap;
+import java.util.logging.Logger;
+
+/**
+ * <code>AndroidInput</code> is one of the main components that connect jme with android. Is derived from GLSurfaceView and handles all Inputs
+ * @author larynx
+ *
+ */
+public class AndroidInput implements
+        TouchInput,
+        View.OnTouchListener,
+        View.OnKeyListener,
+        GestureDetector.OnGestureListener,
+        GestureDetector.OnDoubleTapListener,
+        ScaleGestureDetector.OnScaleGestureListener {
+
+    final private static int MAX_EVENTS = 1024;
+    // Custom settings
+    public boolean mouseEventsEnabled = true;
+    public boolean mouseEventsInvertX = false;
+    public boolean mouseEventsInvertY = false;
+    public boolean keyboardEventsEnabled = false;
+    public boolean dontSendHistory = false;
+    // Used to transfer events from android thread to GLThread
+    final private RingBuffer<TouchEvent> eventQueue = new RingBuffer<TouchEvent>(MAX_EVENTS);
+    final private RingBuffer<TouchEvent> eventPoolUnConsumed = new RingBuffer<TouchEvent>(MAX_EVENTS);
+    final private RingBuffer<TouchEvent> eventPool = new RingBuffer<TouchEvent>(MAX_EVENTS);
+    final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
+    // Internal
+    private View view;
+    private ScaleGestureDetector scaledetector;
+    private boolean scaleInProgress = false;
+    private GestureDetector detector;
+    private int lastX;
+    private int lastY;
+    private final static Logger logger = Logger.getLogger(AndroidInput.class.getName());
+    private boolean isInitialized = false;
+    private RawInputListener listener = null;
+    private static final int[] ANDROID_TO_JME = {
+        0x0, // unknown
+        0x0, // key code soft left
+        0x0, // key code soft right
+        KeyInput.KEY_HOME,
+        KeyInput.KEY_ESCAPE, // key back
+        0x0, // key call
+        0x0, // key endcall
+        KeyInput.KEY_0,
+        KeyInput.KEY_1,
+        KeyInput.KEY_2,
+        KeyInput.KEY_3,
+        KeyInput.KEY_4,
+        KeyInput.KEY_5,
+        KeyInput.KEY_6,
+        KeyInput.KEY_7,
+        KeyInput.KEY_8,
+        KeyInput.KEY_9,
+        KeyInput.KEY_MULTIPLY,
+        0x0, // key pound
+        KeyInput.KEY_UP,
+        KeyInput.KEY_DOWN,
+        KeyInput.KEY_LEFT,
+        KeyInput.KEY_RIGHT,
+        KeyInput.KEY_RETURN, // dpad center
+        0x0, // volume up
+        0x0, // volume down
+        KeyInput.KEY_POWER, // power (?)
+        0x0, // camera
+        0x0, // clear
+        KeyInput.KEY_A,
+        KeyInput.KEY_B,
+        KeyInput.KEY_C,
+        KeyInput.KEY_D,
+        KeyInput.KEY_E,
+        KeyInput.KEY_F,
+        KeyInput.KEY_G,
+        KeyInput.KEY_H,
+        KeyInput.KEY_I,
+        KeyInput.KEY_J,
+        KeyInput.KEY_K,
+        KeyInput.KEY_L,
+        KeyInput.KEY_M,
+        KeyInput.KEY_N,
+        KeyInput.KEY_O,
+        KeyInput.KEY_P,
+        KeyInput.KEY_Q,
+        KeyInput.KEY_R,
+        KeyInput.KEY_S,
+        KeyInput.KEY_T,
+        KeyInput.KEY_U,
+        KeyInput.KEY_V,
+        KeyInput.KEY_W,
+        KeyInput.KEY_X,
+        KeyInput.KEY_Y,
+        KeyInput.KEY_Z,
+        KeyInput.KEY_COMMA,
+        KeyInput.KEY_PERIOD,
+        KeyInput.KEY_LMENU,
+        KeyInput.KEY_RMENU,
+        KeyInput.KEY_LSHIFT,
+        KeyInput.KEY_RSHIFT,
+        //        0x0, // fn
+        //        0x0, // cap (?)
+
+        KeyInput.KEY_TAB,
+        KeyInput.KEY_SPACE,
+        0x0, // sym (?) symbol
+        0x0, // explorer
+        0x0, // envelope
+        KeyInput.KEY_RETURN, // newline/enter
+        KeyInput.KEY_DELETE,
+        KeyInput.KEY_GRAVE,
+        KeyInput.KEY_MINUS,
+        KeyInput.KEY_EQUALS,
+        KeyInput.KEY_LBRACKET,
+        KeyInput.KEY_RBRACKET,
+        KeyInput.KEY_BACKSLASH,
+        KeyInput.KEY_SEMICOLON,
+        KeyInput.KEY_APOSTROPHE,
+        KeyInput.KEY_SLASH,
+        KeyInput.KEY_AT, // at (@)
+        KeyInput.KEY_NUMLOCK, //0x0, // num
+        0x0, //headset hook
+        0x0, //focus
+        KeyInput.KEY_ADD,
+        KeyInput.KEY_LMETA, //menu
+        0x0,//notification
+        0x0,//search
+        0x0,//media play/pause
+        0x0,//media stop
+        0x0,//media next
+        0x0,//media previous
+        0x0,//media rewind
+        0x0,//media fastforward
+        0x0,//mute
+    };
+
+    public AndroidInput() {
+    }
+
+    public void setView(View view) {
+        this.view = view;
+        if (view != null) {
+            detector = new GestureDetector(null, this, null, false);
+            scaledetector = new ScaleGestureDetector(view.getContext(), this);
+            view.setOnTouchListener(this);
+            view.setOnKeyListener(this);
+        }
+    }
+
+    private TouchEvent getNextFreeTouchEvent() {
+        return getNextFreeTouchEvent(false);
+    }
+
+    /**
+     * Fetches a touch event from the reuse pool
+     * @param wait if true waits for a reusable event to get available/released
+     * by an other thread, if false returns a new one if needed.
+     *
+     * @return a usable TouchEvent
+     */
+    private TouchEvent getNextFreeTouchEvent(boolean wait) {
+        TouchEvent evt = null;
+        synchronized (eventPoolUnConsumed) {
+            int size = eventPoolUnConsumed.size();
+            while (size > 0) {
+                evt = eventPoolUnConsumed.pop();
+                if (!evt.isConsumed()) {
+                    eventPoolUnConsumed.push(evt);
+                    evt = null;
+                } else {
+                    break;
+                }
+                size--;
+            }
+        }
+
+        if (evt == null) {
+            if (eventPool.isEmpty() && wait) {
+                logger.warning("eventPool buffer underrun");
+                boolean isEmpty;
+                do {
+                    synchronized (eventPool) {
+                        isEmpty = eventPool.isEmpty();
+                    }
+                    try {
+                        Thread.sleep(50);
+                    } catch (InterruptedException e) {
+                    }
+                } while (isEmpty);
+                synchronized (eventPool) {
+                    evt = eventPool.pop();
+                }
+            } else if (eventPool.isEmpty()) {
+                evt = new TouchEvent();
+                logger.warning("eventPool buffer underrun");
+            } else {
+                synchronized (eventPool) {
+                    evt = eventPool.pop();
+                }
+            }
+        }
+        return evt;
+    }
+
+    /**
+     * onTouch gets called from android thread on touchpad events
+     */
+    public boolean onTouch(View view, MotionEvent event) {
+        if (view != this.view) {
+            return false;
+        }
+        boolean bWasHandled = false;
+        TouchEvent touch;
+        //    System.out.println("native : " + event.getAction());
+        int action = event.getAction() & MotionEvent.ACTION_MASK;
+        int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        int pointerId = event.getPointerId(pointerIndex);
+        Vector2f lastPos = lastPositions.get(pointerId);
+
+        // final int historySize = event.getHistorySize();
+        //final int pointerCount = event.getPointerCount();
+        switch (action) {
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_DOWN:
+                touch = getNextFreeTouchEvent();
+                touch.set(Type.DOWN, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
+                touch.setPointerId(pointerId);
+                touch.setTime(event.getEventTime());
+                touch.setPressure(event.getPressure(pointerIndex));
+                processEvent(touch);
+
+                lastPos = new Vector2f(event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex));
+                lastPositions.put(pointerId, lastPos);
+
+                bWasHandled = true;
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                touch = getNextFreeTouchEvent();
+                touch.set(Type.UP, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
+                touch.setPointerId(pointerId);
+                touch.setTime(event.getEventTime());
+                touch.setPressure(event.getPressure(pointerIndex));
+                processEvent(touch);
+                lastPositions.remove(pointerId);
+
+                bWasHandled = true;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // Convert all pointers into events
+                for (int p = 0; p < event.getPointerCount(); p++) {
+                    lastPos = lastPositions.get(event.getPointerId(p));
+                    if (lastPos == null) {
+                        lastPos = new Vector2f(event.getX(p), view.getHeight() - event.getY(p));
+                        lastPositions.put(event.getPointerId(p), lastPos);
+                    }
+
+                    float dX = event.getX(p) - lastPos.x;
+                    float dY = view.getHeight() - event.getY(p) - lastPos.y;
+                    if (dX != 0 || dY != 0) {
+                        touch = getNextFreeTouchEvent();
+                        touch.set(Type.MOVE, event.getX(p), view.getHeight() - event.getY(p), dX, dY);
+                        touch.setPointerId(event.getPointerId(p));
+                        touch.setTime(event.getEventTime());
+                        touch.setPressure(event.getPressure(p));
+                        touch.setScaleSpanInProgress(scaleInProgress);
+                        processEvent(touch);
+                        lastPos.set(event.getX(p), view.getHeight() - event.getY(p));
+                    }
+                }
+                bWasHandled = true;
+                break;
+            case MotionEvent.ACTION_OUTSIDE:
+                break;
+
+        }
+
+        // Try to detect gestures
+        this.detector.onTouchEvent(event);
+        this.scaledetector.onTouchEvent(event);
+
+        return bWasHandled;
+    }
+
+    /**
+     * onKey gets called from android thread on key events
+     */
+    public boolean onKey(View view, int keyCode, KeyEvent event) {
+        if (view != this.view) {
+            return false;
+        }
+
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+        TouchEvent evt;
+        evt = getNextFreeTouchEvent();
+        evt.set(TouchEvent.Type.KEY_DOWN);
+        evt.setKeyCode(keyCode);
+        evt.setCharacters(event.getCharacters());
+        evt.setTime(event.getEventTime());
+
+        // Send the event
+        processEvent(evt);
+
+        // Handle all keys ourself except Volume Up/Down
+        if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
+            return false;
+        } else {
+            return true;
+        }
+        } else if (event.getAction() == KeyEvent.ACTION_UP) {
+        TouchEvent evt;
+        evt = getNextFreeTouchEvent();
+        evt.set(TouchEvent.Type.KEY_UP);
+        evt.setKeyCode(keyCode);
+        evt.setCharacters(event.getCharacters());
+        evt.setTime(event.getEventTime());
+
+        // Send the event
+        processEvent(evt);
+
+        // Handle all keys ourself except Volume Up/Down
+        if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
+            return false;
+        } else {
+            return true;
+        }
+        } else {
+            return false;
+        }
+    }
+
+    public void loadSettings(AppSettings settings) {
+        mouseEventsEnabled = settings.isEmulateMouse();
+        mouseEventsInvertX = settings.isEmulateMouseFlipX();
+        mouseEventsInvertY = settings.isEmulateMouseFlipY();
+    }
+
+    // -----------------------------------------
+    // JME3 Input interface
+    @Override
+    public void initialize() {
+        TouchEvent item;
+        for (int i = 0; i < MAX_EVENTS; i++) {
+            item = new TouchEvent();
+            eventPool.push(item);
+        }
+        isInitialized = true;
+    }
+
+    @Override
+    public void destroy() {
+        isInitialized = false;
+
+        // Clean up queues
+        while (!eventPool.isEmpty()) {
+            eventPool.pop();
+        }
+        while (!eventQueue.isEmpty()) {
+            eventQueue.pop();
+        }
+
+
+        this.view = null;
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return isInitialized;
+    }
+
+    @Override
+    public void setInputListener(RawInputListener listener) {
+        this.listener = listener;
+    }
+
+    @Override
+    public long getInputTimeNanos() {
+        return System.nanoTime();
+    }
+    // -----------------------------------------
+
+    private void processEvent(TouchEvent event) {
+        synchronized (eventQueue) {
+            //Discarding events when the ring buffer is full to avoid buffer overflow.
+            if(eventQueue.size()< MAX_EVENTS){
+                eventQueue.push(event);
+            }
+            
+        }
+    }
+
+    //  ---------------  INSIDE GLThread  ---------------
+    @Override
+    public void update() {
+        generateEvents();
+    }
+
+    private void generateEvents() {
+        if (listener != null) {
+            TouchEvent event;
+            MouseButtonEvent btn;
+            MouseMotionEvent mot;
+            int newX;
+            int newY;
+
+            while (!eventQueue.isEmpty()) {
+                synchronized (eventQueue) {
+                    event = eventQueue.pop();
+                }
+                if (event != null) {
+                    listener.onTouchEvent(event);
+
+                    if (mouseEventsEnabled) {
+                        if (mouseEventsInvertX) {
+                            newX = view.getWidth() - (int) event.getX();
+                        } else {
+                            newX = (int) event.getX();
+                        }
+
+                        if (mouseEventsInvertY) {
+                            newY = view.getHeight() - (int) event.getY();
+                        } else {
+                            newY = (int) event.getY();
+                        }
+
+                        switch (event.getType()) {
+                            case DOWN:
+                                // Handle mouse down event
+                                btn = new MouseButtonEvent(0, true, newX, newY);
+                                btn.setTime(event.getTime());
+                                listener.onMouseButtonEvent(btn);
+                                // Store current pos
+                                lastX = -1;
+                                lastY = -1;
+                                break;
+
+                            case UP:
+                                // Handle mouse up event
+                                btn = new MouseButtonEvent(0, false, newX, newY);
+                                btn.setTime(event.getTime());
+                                listener.onMouseButtonEvent(btn);
+                                // Store current pos
+                                lastX = -1;
+                                lastY = -1;
+                                break;
+
+                            case SCALE_MOVE:
+                                if (lastX != -1 && lastY != -1) {
+                                    newX = lastX;
+                                    newY = lastY;
+                                }
+                                int wheel = (int) (event.getScaleSpan() / 4f); // scale to match mouse wheel
+                                int dwheel = (int) (event.getDeltaScaleSpan() / 4f); // scale to match mouse wheel
+                                mot = new MouseMotionEvent(newX, newX, 0, 0, wheel, dwheel);
+                                mot.setTime(event.getTime());
+                                listener.onMouseMotionEvent(mot);
+                                lastX = newX;
+                                lastY = newY;
+
+                                break;
+
+                            case MOVE:
+                                if (event.isScaleSpanInProgress()) {
+                                    break;
+                                }
+
+                                int dx;
+                                int dy;
+                                if (lastX != -1) {
+                                    dx = newX - lastX;
+                                    dy = newY - lastY;
+                                } else {
+                                    dx = 0;
+                                    dy = 0;
+                                }
+
+                                mot = new MouseMotionEvent(newX, newY, dx, dy, (int)event.getScaleSpan(), (int)event.getDeltaScaleSpan());
+                                mot.setTime(event.getTime());
+                                listener.onMouseMotionEvent(mot);
+                                lastX = newX;
+                                lastY = newY;
+
+                                break;
+                        }
+                    }
+                }
+
+                if (event.isConsumed() == false) {
+                    synchronized (eventPoolUnConsumed) {
+                        eventPoolUnConsumed.push(event);
+                    }
+
+                } else {
+                    synchronized (eventPool) {
+                        eventPool.push(event);
+                    }
+                }
+            }
+
+        }
+    }
+    //  --------------- ENDOF INSIDE GLThread  ---------------
+
+    // --------------- Gesture detected callback events  ---------------
+    public boolean onDown(MotionEvent event) {
+        return false;
+    }
+
+    public void onLongPress(MotionEvent event) {
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.LONGPRESSED, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
+        touch.setPointerId(0);
+        touch.setTime(event.getEventTime());
+        processEvent(touch);
+    }
+
+    public boolean onFling(MotionEvent event, MotionEvent event2, float vx, float vy) {
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.FLING, event.getX(), view.getHeight() - event.getY(), vx, vy);
+        touch.setPointerId(0);
+        touch.setTime(event.getEventTime());
+        processEvent(touch);
+
+        return true;
+    }
+
+    public boolean onSingleTapConfirmed(MotionEvent event) {
+        //Nothing to do here the tap has already been detected.
+        return false;
+    }
+
+    public boolean onDoubleTap(MotionEvent event) {
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.DOUBLETAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
+        touch.setPointerId(0);
+        touch.setTime(event.getEventTime());
+        processEvent(touch);
+        return true;
+    }
+
+    public boolean onDoubleTapEvent(MotionEvent event) {
+        return false;
+    }
+
+    public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
+        scaleInProgress = true;
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.SCALE_START, scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY(), 0f, 0f);
+        touch.setPointerId(0);
+        touch.setTime(scaleGestureDetector.getEventTime());
+        touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
+        touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
+        touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
+        touch.setScaleSpanInProgress(scaleInProgress);
+        processEvent(touch);
+        //    System.out.println("scaleBegin");
+
+        return true;
+    }
+
+    public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.SCALE_MOVE, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
+        touch.setPointerId(0);
+        touch.setTime(scaleGestureDetector.getEventTime());
+        touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
+        touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
+        touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
+        touch.setScaleSpanInProgress(scaleInProgress);
+        processEvent(touch);
+        //   System.out.println("scale");
+
+        return false;
+    }
+
+    public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
+        scaleInProgress = false;
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.SCALE_END, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
+        touch.setPointerId(0);
+        touch.setTime(scaleGestureDetector.getEventTime());
+        touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
+        touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
+        touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
+        touch.setScaleSpanInProgress(scaleInProgress);
+        processEvent(touch);
+    }
+
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.SCROLL, e1.getX(), view.getHeight() - e1.getY(), distanceX, distanceY * (-1));
+        touch.setPointerId(0);
+        touch.setTime(e1.getEventTime());
+        processEvent(touch);
+        //System.out.println("scroll " + e1.getPointerCount());
+        return false;
+    }
+
+    public void onShowPress(MotionEvent event) {
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.SHOWPRESS, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
+        touch.setPointerId(0);
+        touch.setTime(event.getEventTime());
+        processEvent(touch);
+    }
+
+    public boolean onSingleTapUp(MotionEvent event) {
+        TouchEvent touch = getNextFreeTouchEvent();
+        touch.set(Type.TAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
+        touch.setPointerId(0);
+        touch.setTime(event.getEventTime());
+        processEvent(touch);
+        return true;
+    }
+
+    @Override
+    public void setSimulateKeyboard(boolean simulate) {
+        keyboardEventsEnabled = simulate;
+    }
+
+    @Override
+    public void setOmitHistoricEvents(boolean dontSendHistory) {
+        this.dontSendHistory = dontSendHistory;
+    }
+
+    /**
+     * @deprecated Use {@link #getSimulateMouse()};
+     */
+    @Deprecated
+    public boolean isMouseEventsEnabled() {
+        return mouseEventsEnabled;
+    }
+
+    @Deprecated
+    public void setMouseEventsEnabled(boolean mouseEventsEnabled) {
+        this.mouseEventsEnabled = mouseEventsEnabled;
+    }
+
+    public boolean isMouseEventsInvertY() {
+        return mouseEventsInvertY;
+    }
+
+    public void setMouseEventsInvertY(boolean mouseEventsInvertY) {
+        this.mouseEventsInvertY = mouseEventsInvertY;
+    }
+
+    public boolean isMouseEventsInvertX() {
+        return mouseEventsInvertX;
+    }
+
+    public void setMouseEventsInvertX(boolean mouseEventsInvertX) {
+        this.mouseEventsInvertX = mouseEventsInvertX;
+    }
+
+    public void setSimulateMouse(boolean simulate) {
+        mouseEventsEnabled = simulate;
+    }
+
+    public boolean getSimulateMouse() {
+        return isSimulateMouse();
+    }
+
+    public boolean isSimulateMouse() {
+        return mouseEventsEnabled;
+    }
+
+    public void showVirtualKeyboard(boolean visible) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+}

+ 273 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java

@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.os.Build;
+import android.view.View;
+import com.jme3.input.RawInputListener;
+import com.jme3.input.TouchInput;
+import com.jme3.input.event.InputEvent;
+import com.jme3.input.event.KeyInputEvent;
+import com.jme3.input.event.MouseButtonEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+import com.jme3.renderer.android.AndroidGLSurfaceView;
+import com.jme3.system.AppSettings;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <code>AndroidInput</code> is the main class that connects the Android system
+ * inputs to jME. It serves as the manager that gathers inputs from the various
+ * Android input methods and provides them to jME's <code>InputManager</code>.
+ *
+ * @author iwgeric
+ */
+public class AndroidInputHandler implements TouchInput {
+    private static final Logger logger = Logger.getLogger(AndroidInputHandler.class.getName());
+    
+    // Custom settings
+    private boolean mouseEventsEnabled = true;
+    private boolean mouseEventsInvertX = false;
+    private boolean mouseEventsInvertY = false;
+    private boolean keyboardEventsEnabled = false;
+    private boolean joystickEventsEnabled = false;
+    private boolean dontSendHistory = false;
+    
+    
+    // Internal
+    private AndroidGLSurfaceView view;
+    private AndroidTouchHandler touchHandler;
+    private AndroidKeyHandler keyHandler;
+    private AndroidGestureHandler gestureHandler;
+    private boolean initialized = false;
+    private RawInputListener listener = null;
+    private ConcurrentLinkedQueue<InputEvent> inputEventQueue = new ConcurrentLinkedQueue<InputEvent>();
+    private final static int MAX_TOUCH_EVENTS = 1024;
+    private final TouchEventPool touchEventPool = new TouchEventPool(MAX_TOUCH_EVENTS);
+    private float scaleX = 1f;
+    private float scaleY = 1f;
+    
+    
+    public AndroidInputHandler() {
+        int buildVersion = Build.VERSION.SDK_INT;
+        logger.log(Level.INFO, "Android Build Version: {0}", buildVersion);
+        if (buildVersion >= 14) {
+            // add support for onHover and GenericMotionEvent (ie. gamepads)
+            gestureHandler = new AndroidGestureHandler(this);
+            touchHandler = new AndroidTouchHandler14(this, gestureHandler);
+            keyHandler = new AndroidKeyHandler(this);
+        } else if (buildVersion >= 8){
+            gestureHandler = new AndroidGestureHandler(this);
+            touchHandler = new AndroidTouchHandler(this, gestureHandler);
+            keyHandler = new AndroidKeyHandler(this);
+        }
+    }
+    
+    public AndroidInputHandler(AndroidTouchHandler touchInput, 
+            AndroidKeyHandler keyInput, AndroidGestureHandler gestureHandler) {
+        this.touchHandler = touchInput;
+        this.keyHandler = keyInput;
+        this.gestureHandler = gestureHandler;
+    }
+    
+    public void setView(View view) {
+        if (touchHandler != null) {
+            touchHandler.setView(view);
+        }
+        if (keyHandler != null) {
+            keyHandler.setView(view);
+        }
+        if (gestureHandler != null) {
+            gestureHandler.setView(view);
+        }
+        this.view = (AndroidGLSurfaceView)view;
+    }
+    
+    public View getView() {
+        return view;
+    }
+    
+    public float invertX(float origX) {
+        return getJmeX(view.getWidth()) - origX;
+    }
+    
+    public float invertY(float origY) {
+        return getJmeY(view.getHeight()) - origY;
+    }
+    
+    public float getJmeX(float origX) {
+        return origX * scaleX;
+    }
+    
+    public float getJmeY(float origY) {
+        return origY * scaleY;
+    }
+    
+    public void loadSettings(AppSettings settings) {
+        // TODO: add simulate keyboard to settings
+//        keyboardEventsEnabled = true;
+        mouseEventsEnabled = settings.isEmulateMouse();
+        mouseEventsInvertX = settings.isEmulateMouseFlipX();
+        mouseEventsInvertY = settings.isEmulateMouseFlipY();
+        joystickEventsEnabled = settings.useJoysticks();
+        
+        // view width and height are 0 until the view is displayed on the screen
+        if (view.getWidth() != 0 && view.getHeight() != 0) {
+            scaleX = (float)settings.getWidth() / (float)view.getWidth();
+            scaleY = (float)settings.getHeight() / (float)view.getHeight();
+        }
+        logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}", 
+                new Object[]{scaleX, scaleY});
+        
+    }
+
+        // -----------------------------------------
+    // JME3 Input interface
+    @Override
+    public void initialize() {
+        touchEventPool.initialize();
+        if (touchHandler != null) {
+            touchHandler.initialize();
+        }
+        if (keyHandler != null) {
+            keyHandler.initialize();
+        }
+        if (gestureHandler != null) {
+            gestureHandler.initialize();
+        }
+        
+        initialized = true;
+    }
+
+    @Override
+    public void destroy() {
+        initialized = false;
+        
+        touchEventPool.destroy();
+        if (touchHandler != null) {
+            touchHandler.destroy();
+        }
+        if (keyHandler != null) {
+            keyHandler.destroy();
+        }
+        if (gestureHandler != null) {
+            gestureHandler.destroy();
+        }
+        
+        setView(null);
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    @Override
+    public void setInputListener(RawInputListener listener) {
+        this.listener = listener;
+    }
+
+    @Override
+    public long getInputTimeNanos() {
+        return System.nanoTime();
+    }
+    
+    public void update() {
+        if (listener != null) {
+            InputEvent inputEvent;
+            
+            while ((inputEvent = inputEventQueue.poll()) != null) {
+                if (inputEvent instanceof TouchEvent) {
+                    listener.onTouchEvent((TouchEvent)inputEvent);
+                } else if (inputEvent instanceof MouseButtonEvent) {
+                    listener.onMouseButtonEvent((MouseButtonEvent)inputEvent);
+                } else if (inputEvent instanceof MouseMotionEvent) {
+                    listener.onMouseMotionEvent((MouseMotionEvent)inputEvent);
+                } else if (inputEvent instanceof KeyInputEvent) {
+                    listener.onKeyEvent((KeyInputEvent)inputEvent);
+                }
+            }
+        }
+    }
+
+    // -----------------------------------------
+    
+    public TouchEvent getFreeTouchEvent() {
+            return touchEventPool.getNextFreeEvent();
+    }
+    
+    public void addEvent(InputEvent event) {
+        inputEventQueue.add(event);
+        if (event instanceof TouchEvent) {
+            touchEventPool.storeEvent((TouchEvent)event);
+        }
+    }
+
+    public void setSimulateMouse(boolean simulate) {
+        this.mouseEventsEnabled = simulate;
+    }
+
+    public boolean isSimulateMouse() {
+        return mouseEventsEnabled;
+    }
+
+    public boolean getSimulateMouse() {
+        return mouseEventsEnabled;
+    }
+    
+    public boolean isMouseEventsInvertX() {
+        return mouseEventsInvertX;
+    }
+
+    public boolean isMouseEventsInvertY() {
+        return mouseEventsInvertY;
+    }
+    
+    public void setSimulateKeyboard(boolean simulate) {
+        this.keyboardEventsEnabled = simulate;
+    }
+
+    public void setOmitHistoricEvents(boolean dontSendHistory) {
+        this.dontSendHistory = dontSendHistory;
+    }
+
+    public void showVirtualKeyboard(boolean visible) {
+        if (keyHandler != null) {
+            keyHandler.showVirtualKeyboard(visible);
+        }
+    }
+
+
+}

+ 156 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidKeyHandler.java

@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.content.Context;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import com.jme3.input.event.KeyInputEvent;
+import com.jme3.input.event.TouchEvent;
+import java.util.logging.Logger;
+
+/**
+ * AndroidKeyHandler recieves onKey events from the Android system and creates
+ * the jME KeyEvents.  onKey is used by Android to receive keys from the keyboard
+ * or device buttons.  All key events are consumed by jME except for the Volume
+ * buttons and menu button.
+ * 
+ * This class also provides the functionality to display or hide the soft keyboard
+ * for inputing single key events.  Use OGLESContext to display an dialog to type
+ * in complete strings.
+ * 
+ * @author iwgeric
+ */
+public class AndroidKeyHandler implements View.OnKeyListener {
+    private static final Logger logger = Logger.getLogger(AndroidKeyHandler.class.getName());
+    
+    private AndroidInputHandler androidInput;
+    private boolean sendKeyEvents = true;
+    
+    public AndroidKeyHandler(AndroidInputHandler androidInput) {
+        this.androidInput = androidInput;
+    }
+    
+    public void initialize() {
+    }
+    
+    public void destroy() {
+    }
+    
+    public void setView(View view) {
+        if (view != null) {
+            view.setOnKeyListener(this);
+        } else {
+            androidInput.getView().setOnKeyListener(null);
+        }
+    }
+    
+    /**
+     * onKey gets called from android thread on key events
+     */
+    public boolean onKey(View view, int keyCode, KeyEvent event) {
+        if (androidInput.isInitialized() && view != androidInput.getView()) {
+            return false;
+        }
+        
+        TouchEvent evt;
+        // TODO: get touch event from pool
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            evt = new TouchEvent();
+            evt.set(TouchEvent.Type.KEY_DOWN);
+            evt.setKeyCode(keyCode);
+            evt.setCharacters(event.getCharacters());
+            evt.setTime(event.getEventTime());
+
+            // Send the event
+            androidInput.addEvent(evt);
+
+        } else if (event.getAction() == KeyEvent.ACTION_UP) {
+            evt = new TouchEvent();
+            evt.set(TouchEvent.Type.KEY_UP);
+            evt.setKeyCode(keyCode);
+            evt.setCharacters(event.getCharacters());
+            evt.setTime(event.getEventTime());
+
+            // Send the event
+            androidInput.addEvent(evt);
+
+        }
+        
+        
+        KeyInputEvent kie;
+        char unicodeChar = (char)event.getUnicodeChar();
+        int jmeKeyCode = AndroidKeyMapping.getJmeKey(keyCode);
+        
+        boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
+        boolean repeating = pressed && event.getRepeatCount() > 0;
+
+        kie = new KeyInputEvent(jmeKeyCode, unicodeChar, pressed, repeating);
+        kie.setTime(event.getEventTime());
+        androidInput.addEvent(kie);
+//        logger.log(Level.FINE, "onKey keyCode: {0}, jmeKeyCode: {1}, pressed: {2}, repeating: {3}", 
+//                new Object[]{keyCode, jmeKeyCode, pressed, repeating});
+//        logger.log(Level.FINE, "creating KeyInputEvent: {0}", kie);
+        
+        // consume all keys ourself except Volume Up/Down and Menu
+        //   Don't do Menu so that typical Android Menus can be created and used
+        //   by the user in MainActivity
+        if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || 
+                (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) || 
+                (keyCode == KeyEvent.KEYCODE_MENU)) {
+            return false;
+        } else {
+            return true;
+        }
+   }
+    
+    public void showVirtualKeyboard (final boolean visible) {
+        androidInput.getView().getHandler().post(new Runnable() {
+
+            public void run() {
+                InputMethodManager manager = 
+                        (InputMethodManager)androidInput.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+
+                if (visible) {
+                    manager.showSoftInput(androidInput.getView(), 0);
+                    sendKeyEvents = true;
+                } else {
+                    manager.hideSoftInputFromWindow(androidInput.getView().getWindowToken(), 0);
+                    sendKeyEvents = false;
+                }
+            }
+        });
+    }
+    
+}

+ 149 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java

@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import com.jme3.input.KeyInput;
+import java.util.logging.Logger;
+
+/**
+ * AndroidKeyMapping is just a utility to convert the Android keyCodes into
+ * jME KeyCodes received in jME's KeyEvent will match between Desktop and Android.
+ * 
+ * @author iwgeric
+ */
+public class AndroidKeyMapping {
+    private static final Logger logger = Logger.getLogger(AndroidKeyMapping.class.getName());
+    
+    private static final int[] ANDROID_TO_JME = {
+        0x0, // unknown
+        0x0, // key code soft left
+        0x0, // key code soft right
+        KeyInput.KEY_HOME,
+        KeyInput.KEY_ESCAPE, // key back
+        0x0, // key call
+        0x0, // key endcall
+        KeyInput.KEY_0,
+        KeyInput.KEY_1,
+        KeyInput.KEY_2,
+        KeyInput.KEY_3,
+        KeyInput.KEY_4,
+        KeyInput.KEY_5,
+        KeyInput.KEY_6,
+        KeyInput.KEY_7,
+        KeyInput.KEY_8,
+        KeyInput.KEY_9,
+        KeyInput.KEY_MULTIPLY,
+        0x0, // key pound
+        KeyInput.KEY_UP,
+        KeyInput.KEY_DOWN,
+        KeyInput.KEY_LEFT,
+        KeyInput.KEY_RIGHT,
+        KeyInput.KEY_RETURN, // dpad center
+        0x0, // volume up
+        0x0, // volume down
+        KeyInput.KEY_POWER, // power (?)
+        0x0, // camera
+        0x0, // clear
+        KeyInput.KEY_A,
+        KeyInput.KEY_B,
+        KeyInput.KEY_C,
+        KeyInput.KEY_D,
+        KeyInput.KEY_E,
+        KeyInput.KEY_F,
+        KeyInput.KEY_G,
+        KeyInput.KEY_H,
+        KeyInput.KEY_I,
+        KeyInput.KEY_J,
+        KeyInput.KEY_K,
+        KeyInput.KEY_L,
+        KeyInput.KEY_M,
+        KeyInput.KEY_N,
+        KeyInput.KEY_O,
+        KeyInput.KEY_P,
+        KeyInput.KEY_Q,
+        KeyInput.KEY_R,
+        KeyInput.KEY_S,
+        KeyInput.KEY_T,
+        KeyInput.KEY_U,
+        KeyInput.KEY_V,
+        KeyInput.KEY_W,
+        KeyInput.KEY_X,
+        KeyInput.KEY_Y,
+        KeyInput.KEY_Z,
+        KeyInput.KEY_COMMA,
+        KeyInput.KEY_PERIOD,
+        KeyInput.KEY_LMENU,
+        KeyInput.KEY_RMENU,
+        KeyInput.KEY_LSHIFT,
+        KeyInput.KEY_RSHIFT,
+        //        0x0, // fn
+        //        0x0, // cap (?)
+
+        KeyInput.KEY_TAB,
+        KeyInput.KEY_SPACE,
+        0x0, // sym (?) symbol
+        0x0, // explorer
+        0x0, // envelope
+        KeyInput.KEY_RETURN, // newline/enter
+        KeyInput.KEY_BACK, //used to be KeyInput.KEY_DELETE,
+        KeyInput.KEY_GRAVE,
+        KeyInput.KEY_MINUS,
+        KeyInput.KEY_EQUALS,
+        KeyInput.KEY_LBRACKET,
+        KeyInput.KEY_RBRACKET,
+        KeyInput.KEY_BACKSLASH,
+        KeyInput.KEY_SEMICOLON,
+        KeyInput.KEY_APOSTROPHE,
+        KeyInput.KEY_SLASH,
+        KeyInput.KEY_AT, // at (@)
+        KeyInput.KEY_NUMLOCK, //0x0, // num
+        0x0, //headset hook
+        0x0, //focus
+        KeyInput.KEY_ADD,
+        KeyInput.KEY_LMETA, //menu
+        0x0,//notification
+        0x0,//search
+        0x0,//media play/pause
+        0x0,//media stop
+        0x0,//media next
+        0x0,//media previous
+        0x0,//media rewind
+        0x0,//media fastforward
+        0x0,//mute
+    };
+    
+    public static int getJmeKey(int androidKey) {
+        return ANDROID_TO_JME[androidKey];
+    }
+    
+}

+ 795 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java

@@ -0,0 +1,795 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Vibrator;
+import android.view.Surface;
+import android.view.View;
+import com.jme3.input.AbstractJoystick;
+import com.jme3.input.DefaultJoystickAxis;
+import com.jme3.input.InputManager;
+import com.jme3.input.JoyInput;
+import com.jme3.input.Joystick;
+import com.jme3.input.JoystickAxis;
+import com.jme3.input.SensorJoystickAxis;
+import com.jme3.input.RawInputListener;
+import com.jme3.input.event.JoyAxisEvent;
+import com.jme3.math.FastMath;
+import com.jme3.system.android.JmeAndroidSystem;
+import com.jme3.util.IntMap;
+import com.jme3.util.IntMap.Entry;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * AndroidSensorJoyInput converts the Android Sensor system into Joystick events.
+ * A single joystick is configured and includes data for all configured sensors
+ * as seperate axes of the joystick.
+ *
+ * Each axis is named accounting to the static strings in SensorJoystickAxis.
+ * Refer to the strings defined in SensorJoystickAxis for a list of supported
+ * sensors and their axis data.  Each sensor type defined in SensorJoystickAxis
+ * will be attempted to be configured.  If the device does not support a particular
+ * sensor, the axis will return null if joystick.getAxis(String name) is called.
+ *
+ * The joystick.getXAxis and getYAxis methods of the joystick are configured to
+ * return the device orientation values in the device's X and Y directions.
+ *
+ * This joystick also supports the joystick.rumble(rumbleAmount) method.  In this
+ * case, when joystick.rumble(rumbleAmount) is called, the Android device will vibrate
+ * if the device has a built in vibrate motor.
+ *
+ * Because Andorid does not allow for the user to define the intensity of the
+ * vibration, the rumble amount (ie strength) is converted into vibration pulses
+ * The stronger the strength amount, the shorter the delay between pulses.  If
+ * amount is 1, then the vibration stays on the whole time.  If amount is 0.5,
+ * the vibration will a pulse of equal parts vibration and delay.
+ * To turn off vibration, set rumble amount to 0.
+ *
+ * MainActivity needs the following line to enable Joysticks on Android platforms
+ *    joystickEventsEnabled = true;
+ * This is done to allow for battery conservation when sensor data is not required
+ * by the application.
+ *
+ * To use the joystick rumble feature, the following line needs to be
+ * added to the Android Manifest File
+ *     <uses-permission android:name="android.permission.VIBRATE"/>
+ *
+ * @author iwgeric
+ */
+public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
+    private final static Logger logger = Logger.getLogger(AndroidSensorJoyInput.class.getName());
+
+    private Activity activity = null;
+    private InputManager inputManager = null;
+    private SensorManager sensorManager = null;
+    private Vibrator vibrator = null;
+    private boolean vibratorActive = false;
+    private long maxRumbleTime = 250;  // 250ms
+    private RawInputListener listener = null;
+    private IntMap<SensorData> sensors = new IntMap<SensorData>();
+    private AndroidJoystick[] joysticks;
+    private int lastRotation = 0;
+    private boolean initialized = false;
+    private boolean loaded = false;
+
+    private final ArrayList<JoyAxisEvent> eventQueue = new ArrayList<JoyAxisEvent>();
+
+    /**
+     * Internal class to enclose data for each sensor.
+     */
+    private class SensorData {
+        int androidSensorType = -1;
+        int androidSensorSpeed = SensorManager.SENSOR_DELAY_GAME;
+        Sensor sensor = null;
+        int sensorAccuracy = 0;
+        float[] lastValues;
+        final Object valuesLock = new Object();
+        ArrayList<AndroidJoystickAxis> axes = new ArrayList<AndroidJoystickAxis>();
+        boolean enabled = false;
+        boolean haveData = false;
+
+        public SensorData(int androidSensorType, Sensor sensor) {
+            this.androidSensorType = androidSensorType;
+            this.sensor = sensor;
+        }
+
+    }
+
+    private void initSensorManager() {
+        this.activity = JmeAndroidSystem.getActivity();
+        // Get instance of the SensorManager from the current Context
+        sensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE);
+        // Get instance of Vibrator from current Context
+        vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
+        if (vibrator == null) {
+            logger.log(Level.FINE, "Vibrator Service not found.");
+        }
+    }
+
+    private SensorData initSensor(int sensorType) {
+        boolean success = false;
+
+        SensorData sensorData = sensors.get(sensorType);
+        if (sensorData != null) {
+            unRegisterListener(sensorType);
+        } else {
+            sensorData = new SensorData(sensorType, null);
+            sensors.put(sensorType, sensorData);
+        }
+
+        sensorData.androidSensorType = sensorType;
+        sensorData.sensor = sensorManager.getDefaultSensor(sensorType);
+
+        if (sensorData.sensor != null) {
+            logger.log(Level.FINE, "Sensor Type {0} found.", sensorType);
+            success = registerListener(sensorType);
+        } else {
+            logger.log(Level.FINE, "Sensor Type {0} not found.", sensorType);
+        }
+
+        if (success) {
+            return sensorData;
+        } else {
+            return null;
+        }
+    }
+
+    private boolean registerListener(int sensorType) {
+        SensorData sensorData = sensors.get(sensorType);
+        if (sensorData != null) {
+            if (sensorData.enabled) {
+                logger.log(Level.FINE, "Sensor Already Active: SensorType: {0}, active: {1}",
+                        new Object[]{sensorType, sensorData.enabled});
+                return true;
+            }
+            sensorData.haveData = false;
+            if (sensorData.sensor != null) {
+                if (sensorManager.registerListener(this, sensorData.sensor, sensorData.androidSensorSpeed)) {
+                    sensorData.enabled = true;
+                    logger.log(Level.FINE, "SensorType: {0}, actived: {1}",
+                            new Object[]{sensorType, sensorData.enabled});
+                    return true;
+                } else {
+                    sensorData.enabled = false;
+                    logger.log(Level.FINE, "Sensor Type {0} activation failed.", sensorType);
+                }
+            }
+        }
+        return false;
+    }
+
+    private void unRegisterListener(int sensorType) {
+        SensorData sensorData = sensors.get(sensorType);
+        if (sensorData != null) {
+            if (sensorData.sensor != null) {
+                sensorManager.unregisterListener(this, sensorData.sensor);
+            }
+            sensorData.enabled = false;
+            sensorData.haveData = false;
+            logger.log(Level.FINE, "SensorType: {0} deactivated, active: {1}",
+                    new Object[]{sensorType, sensorData.enabled});
+        }
+    }
+
+    /**
+     * Pauses the sensors to save battery life if the sensors are not needed.
+     * Used to pause sensors when the activity pauses
+     */
+    public void pauseSensors() {
+        for (Entry entry: sensors) {
+            if (entry.getKey() != Sensor.TYPE_ORIENTATION) {
+                unRegisterListener(entry.getKey());
+            }
+        }
+        if (vibrator != null && vibratorActive) {
+            vibrator.cancel();
+        }
+    }
+
+    /**
+     * Resumes the sensors.
+     * Used to resume sensors when the activity comes to the top of the stack
+     */
+    public void resumeSensors() {
+        for (Entry entry: sensors) {
+            if (entry.getKey() != Sensor.TYPE_ORIENTATION) {
+                registerListener(entry.getKey());
+            }
+        }
+    }
+
+    /*
+     * Allows the orientation data to be rotated based on the current device
+     * rotation.  This keeps the data aligned with the game when the user
+     * rotates the device during game play.
+     *
+     * Android remapCoordinateSystem from the Android docs
+     * remapCoordinateSystem(float[] inR, int X, int Y, float[] outR)
+     *
+     * @param   inR   the rotation matrix to be transformed. Usually it is the matrix
+     *          returned by getRotationMatrix(float[], float[], float[], float[]).
+     *
+     * @param   outR  the transformed rotation matrix. inR and outR can be the same
+     *          array, but it is not recommended for performance reason.
+     *
+     * X     defines on which world (Earth) axis and direction the X axis of the device is mapped.
+     * Y     defines on which world (Earth) axis and direction the Y axis of the device is mapped.
+     *
+     * @return True if successful
+     */
+    private boolean remapCoordinates(float[] inR, float[] outR) {
+        int xDir = SensorManager.AXIS_X;
+        int yDir = SensorManager.AXIS_Y;
+        int curRotation = getScreenRotation();
+        if (lastRotation != curRotation) {
+            logger.log(Level.FINE, "Device Rotation changed to: {0}", curRotation);
+        }
+        lastRotation = curRotation;
+
+//        logger.log(Level.FINE, "Screen Rotation: {0}", getScreenRotation());
+        switch (getScreenRotation()) {
+            // device natural position
+            case Surface.ROTATION_0:
+                xDir = SensorManager.AXIS_X;
+                yDir = SensorManager.AXIS_Y;
+                break;
+            // device rotated 90 deg counterclockwise
+            case Surface.ROTATION_90:
+                xDir = SensorManager.AXIS_Y;
+                yDir = SensorManager.AXIS_MINUS_X;
+                break;
+            // device rotated 180 deg counterclockwise
+            case Surface.ROTATION_180:
+                xDir = SensorManager.AXIS_MINUS_X;
+                yDir = SensorManager.AXIS_MINUS_Y;
+                break;
+            // device rotated 270 deg counterclockwise
+            case Surface.ROTATION_270:
+                xDir = SensorManager.AXIS_MINUS_Y;
+                yDir = SensorManager.AXIS_X;
+                break;
+            default:
+                break;
+        }
+        return SensorManager.remapCoordinateSystem(inR, xDir, yDir, outR);
+    }
+
+    /**
+     * Returns the current device rotation.
+     * Surface.ROTATION_0 = device in natural default rotation
+     * Surface.ROTATION_90 = device in rotated 90deg counterclockwise
+     * Surface.ROTATION_180 = device in rotated 180deg counterclockwise
+     * Surface.ROTATION_270 = device in rotated 270deg counterclockwise
+     *
+     * When the Manifest locks the orientation, this value will not change during
+     * gametime, but if the orientation of the screen is based off the sensor,
+     * this value will change as the device is rotated.
+     * @return Current device rotation amount
+     */
+    private int getScreenRotation() {
+        return activity.getWindowManager().getDefaultDisplay().getRotation();
+    }
+
+    /**
+     * Calculates the device orientation based off the data recieved from the
+     * Acceleration Sensor and Mangetic Field sensor
+     * Values are returned relative to the Earth.
+     *
+     * From the Android Doc
+     *
+     * Computes the device's orientation based on the rotation matrix. When it returns, the array values is filled with the result:
+     *  values[0]: azimuth, rotation around the Z axis.
+     *  values[1]: pitch, rotation around the X axis.
+     *  values[2]: roll, rotation around the Y axis.
+     *
+     * The reference coordinate-system used is different from the world
+     * coordinate-system defined for the rotation matrix:
+     *  X is defined as the vector product Y.Z (It is tangential to the ground at the device's current location and roughly points West).
+     *  Y is tangential to the ground at the device's current location and points towards the magnetic North Pole.
+     *  Z points towards the center of the Earth and is perpendicular to the ground.
+     *
+     * @return True if Orientation was calculated
+     */
+    private boolean updateOrientation() {
+        SensorData sensorData;
+        AndroidJoystickAxis axis;
+        final float[] curInclinationMat = new float[16];
+        final float[] curRotationMat = new float[16];
+        final float[] rotatedRotationMat = new float[16];
+        final float[] accValues = new float[3];
+        final float[] magValues = new float[3];
+        final float[] orderedOrientation = new float[3];
+
+        // if the Gravity Sensor is available, use it for orientation, if not
+        // use the accelerometer
+        // NOTE: Seemed to work worse, so just using accelerometer
+//        sensorData = sensors.get(Sensor.TYPE_GRAVITY);
+//        if (sensorData == null) {
+            sensorData = sensors.get(Sensor.TYPE_ACCELEROMETER);
+//        }
+
+        if (sensorData == null || !sensorData.enabled || !sensorData.haveData) {
+            return false;
+        }
+
+        if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
+            return false;
+        }
+
+        synchronized(sensorData.valuesLock) {
+            accValues[0] = sensorData.lastValues[0];
+            accValues[1] = sensorData.lastValues[1];
+            accValues[2] = sensorData.lastValues[2];
+        }
+
+        sensorData = sensors.get(Sensor.TYPE_MAGNETIC_FIELD);
+        if (sensorData == null || !sensorData.enabled || !sensorData.haveData) {
+            return false;
+        }
+
+        if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
+            return false;
+        }
+
+        synchronized(sensorData.valuesLock) {
+            magValues[0] = sensorData.lastValues[0];
+            magValues[1] = sensorData.lastValues[1];
+            magValues[2] = sensorData.lastValues[2];
+        }
+
+        if (SensorManager.getRotationMatrix(curRotationMat, curInclinationMat, accValues, magValues)) {
+            final float [] orientValues = new float[3];
+            if (remapCoordinates(curRotationMat, rotatedRotationMat)) {
+                SensorManager.getOrientation(rotatedRotationMat, orientValues);
+//                logger.log(Level.FINE, "Orientation Values: {0}, {1}, {2}",
+//                        new Object[]{orientValues[0], orientValues[1], orientValues[2]});
+
+
+                // need to reorder to make it x, y, z order instead of z, x, y order
+                orderedOrientation[0] = orientValues[1];
+                orderedOrientation[1] = orientValues[2];
+                orderedOrientation[2] = orientValues[0];
+
+                sensorData = sensors.get(Sensor.TYPE_ORIENTATION);
+                if (sensorData != null && sensorData.axes.size() > 0) {
+                    for (int i=0; i<orderedOrientation.length; i++) {
+                        axis = sensorData.axes.get(i);
+                        if (axis != null) {
+                            axis.setCurRawValue(orderedOrientation[i]);
+                            if (!sensorData.haveData) {
+                                sensorData.haveData = true;
+                            } else {
+                                synchronized (eventQueue){
+                                    if (axis.isChanged()) {
+                                        eventQueue.add(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } else if (sensorData != null) {
+                    if (!sensorData.haveData) {
+                        sensorData.haveData = true;
+                    }
+                }
+
+                return true;
+            } else {
+                logger.log(Level.FINE, "remapCoordinateSystem failed");
+            }
+
+        } else {
+            logger.log(Level.FINE, "getRotationMatrix returned false");
+        }
+
+        return false;
+    }
+
+    // Start of JoyInput methods
+
+    public void setJoyRumble(int joyId, float amount) {
+        // convert amount to pulses since Android doesn't allow intensity
+        if (vibrator != null) {
+            final long rumbleOnDur = (long)(amount * maxRumbleTime); // ms to pulse vibration on
+            final long rumbleOffDur = maxRumbleTime - rumbleOnDur; // ms to delay between pulses
+            final long[] rumblePattern = {
+                0, // start immediately
+                rumbleOnDur, // time to leave vibration on
+                rumbleOffDur // time to delay between vibrations
+            };
+            final int rumbleRepeatFrom = 0; // index into rumble pattern to repeat from
+
+            logger.log(Level.FINE, "Rumble amount: {0}, rumbleOnDur: {1}, rumbleOffDur: {2}",
+                    new Object[]{amount, rumbleOnDur, rumbleOffDur});
+
+            if (rumbleOnDur > 0) {
+                vibrator.vibrate(rumblePattern, rumbleRepeatFrom);
+                vibratorActive = true;
+            } else {
+                vibrator.cancel();
+                vibratorActive = false;
+            }
+        }
+
+    }
+
+    public Joystick[] loadJoysticks(InputManager inputManager) {
+        this.inputManager = inputManager;
+
+        initSensorManager();
+
+        SensorData sensorData;
+        List<Joystick> list = new ArrayList<Joystick>();
+        AndroidJoystick joystick;
+        AndroidJoystickAxis axis;
+
+        joystick = new AndroidJoystick(inputManager,
+                                    this,
+                                    list.size(),
+                                    "AndroidSensorsJoystick");
+        list.add(joystick);
+
+        List<Sensor> availSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
+        for (Sensor sensor: availSensors) {
+            logger.log(Level.FINE, "{0} Sensor is available, Type: {1}, Vendor: {2}, Version: {3}",
+                    new Object[]{sensor.getName(), sensor.getType(), sensor.getVendor(), sensor.getVersion()});
+        }
+
+        // manually create orientation sensor data since orientation is not a physical sensor
+        sensorData = new SensorData(Sensor.TYPE_ORIENTATION, null);
+        sensorData.lastValues = new float[3];
+        sensors.put(Sensor.TYPE_ORIENTATION, sensorData);
+        axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_X, SensorJoystickAxis.ORIENTATION_X, joystick.getAxisCount(), FastMath.HALF_PI);
+        joystick.setYAxis(axis); // joystick y axis = rotation around device x axis
+        sensorData.axes.add(axis);
+        axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_Y, SensorJoystickAxis.ORIENTATION_Y, joystick.getAxisCount(), FastMath.HALF_PI);
+        joystick.setXAxis(axis); // joystick x axis = rotation around device y axis
+        sensorData.axes.add(axis);
+        axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_Z, SensorJoystickAxis.ORIENTATION_Z, joystick.getAxisCount(), FastMath.HALF_PI);
+        sensorData.axes.add(axis);
+
+        // add axes for physical sensors
+        sensorData = initSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        if (sensorData != null) {
+            sensorData.lastValues = new float[3];
+            sensors.put(Sensor.TYPE_MAGNETIC_FIELD, sensorData);
+//            axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_X, "MagneticField_X", joystick.getAxisCount(), 1f);
+//            sensorData.axes.add(axis);
+//            axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_Y, "MagneticField_Y", joystick.getAxisCount(), 1f);
+//            sensorData.axes.add(axis);
+//            axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_Z, "MagneticField_Z", joystick.getAxisCount(), 1f);
+//            sensorData.axes.add(axis);
+        }
+
+        sensorData = initSensor(Sensor.TYPE_ACCELEROMETER);
+        if (sensorData != null) {
+            sensorData.lastValues = new float[3];
+            sensors.put(Sensor.TYPE_ACCELEROMETER, sensorData);
+//            axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_X, "Accelerometer_X", joystick.getAxisCount(), 1f);
+//            sensorData.axes.add(axis);
+//            axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_Y, "Accelerometer_Y", joystick.getAxisCount(), 1f);
+//            sensorData.axes.add(axis);
+//            axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_Z, "Accelerometer_Z", joystick.getAxisCount(), 1f);
+//            sensorData.axes.add(axis);
+        }
+
+//        sensorData = initSensor(Sensor.TYPE_GYROSCOPE);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[3];
+//        }
+//
+//        sensorData = initSensor(Sensor.TYPE_GRAVITY);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[3];
+//        }
+//
+//        sensorData = initSensor(Sensor.TYPE_LINEAR_ACCELERATION);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[3];
+//        }
+//
+//        sensorData = initSensor(Sensor.TYPE_ROTATION_VECTOR);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[4];
+//        }
+//
+//        sensorData = initSensor(Sensor.TYPE_PROXIMITY);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[1];
+//        }
+//
+//        sensorData = initSensor(Sensor.TYPE_LIGHT);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[1];
+//        }
+//
+//        sensorData = initSensor(Sensor.TYPE_PRESSURE);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[1];
+//        }
+//
+//        sensorData = initSensor(Sensor.TYPE_TEMPERATURE);
+//        if (sensorData != null) {
+//            sensorData.lastValues = new float[1];
+//        }
+
+
+        joysticks = list.toArray( new AndroidJoystick[list.size()] );
+        loaded = true;
+        return joysticks;
+    }
+
+    public void initialize() {
+        initialized = true;
+        loaded = false;
+    }
+
+    public void update() {
+        if (!loaded) {
+            return;
+        }
+        updateOrientation();
+        synchronized (eventQueue){
+            // flush events to listener
+            if (listener != null && eventQueue.size() > 0) {
+                for (int i = 0; i < eventQueue.size(); i++){
+                    listener.onJoyAxisEvent(eventQueue.get(i));
+                }
+                eventQueue.clear();
+            }
+        }
+    }
+
+    public void destroy() {
+        logger.log(Level.FINE, "Doing Destroy.");
+        pauseSensors();
+        if (sensorManager != null) {
+            sensorManager.unregisterListener(this);
+        }
+        sensors.clear();
+        eventQueue.clear();
+        initialized = false;
+        loaded = false;
+        joysticks = null;
+        sensorManager = null;
+        vibrator = null;
+        activity = null;
+    }
+
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    public void setInputListener(RawInputListener listener) {
+        this.listener = listener;
+    }
+
+    public long getInputTimeNanos() {
+        return System.nanoTime();
+    }
+
+    // End of JoyInput methods
+
+    // Start of Android SensorEventListener methods
+
+    public void onSensorChanged(SensorEvent se) {
+        if (!initialized || !loaded) {
+            return;
+        }
+
+        int sensorType = se.sensor.getType();
+
+        SensorData sensorData = sensors.get(sensorType);
+        if (sensorData != null && sensorData.sensor.equals(se.sensor) && sensorData.enabled) {
+
+            if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
+                return;
+            }
+            synchronized(sensorData.valuesLock) {
+                for (int i=0; i<sensorData.lastValues.length; i++) {
+                    sensorData.lastValues[i] = se.values[i];
+                }
+            }
+
+            if (sensorData != null && sensorData.axes.size() > 0) {
+                AndroidJoystickAxis axis;
+                for (int i=0; i<se.values.length; i++) {
+                    axis = sensorData.axes.get(i);
+                    if (axis != null) {
+                        axis.setCurRawValue(se.values[i]);
+                        if (!sensorData.haveData) {
+                            sensorData.haveData = true;
+                        } else {
+                            synchronized (eventQueue){
+                                if (axis.isChanged()) {
+                                    eventQueue.add(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
+                                }
+                            }
+                        }
+                    }
+                }
+            } else if (sensorData != null) {
+                if (!sensorData.haveData) {
+                    sensorData.haveData = true;
+                }
+            }
+
+        }
+    }
+
+    public void onAccuracyChanged(Sensor sensor, int i) {
+        int sensorType = sensor.getType();
+        SensorData sensorData = sensors.get(sensorType);
+        if (sensorData != null) {
+            logger.log(Level.FINE, "onAccuracyChanged for {0}: accuracy: {1}",
+                    new Object[]{sensor.getName(), i});
+            logger.log(Level.FINE, "MaxRange: {0}, Resolution: {1}",
+                    new Object[]{sensor.getMaximumRange(), sensor.getResolution()});
+            sensorData.sensorAccuracy = i;
+        }
+    }
+
+    // End of SensorEventListener methods
+
+    protected class AndroidJoystick extends AbstractJoystick {
+        private JoystickAxis nullAxis;
+        private JoystickAxis xAxis;
+        private JoystickAxis yAxis;
+        private JoystickAxis povX;
+        private JoystickAxis povY;
+
+        public AndroidJoystick( InputManager inputManager, JoyInput joyInput,
+                                int joyId, String name){
+
+            super( inputManager, joyInput, joyId, name );
+
+            this.nullAxis = new DefaultJoystickAxis( getInputManager(), this, -1,
+                                                     "Null", "null", false, false, 0 );
+            this.xAxis = nullAxis;
+            this.yAxis = nullAxis;
+            this.povX = nullAxis;
+            this.povY = nullAxis;
+
+        }
+
+        protected AndroidJoystickAxis addAxis(String axisName, String logicalName, int axisNum, float maxRawValue) {
+            AndroidJoystickAxis axis;
+
+            axis = new AndroidJoystickAxis(
+                    inputManager,               // InputManager (InputManager)
+                    this,                       // parent Joystick (Joystick)
+                    axisNum,                    // Axis Index (int)
+                    axisName,                   // Axis Name (String)
+                    logicalName,                // Logical ID (String)
+                    true,                       // isAnalog (boolean)
+                    false,                      // isRelative (boolean)
+                    0.01f,                      // Axis Deadzone (float)
+                    maxRawValue);               // Axis Max Raw Value (float)
+
+            super.addAxis(axis);
+
+            return axis;
+        }
+
+        protected void setXAxis(JoystickAxis axis) {
+            xAxis = axis;
+        }
+        protected void setYAxis(JoystickAxis axis) {
+            yAxis = axis;
+        }
+
+        @Override
+        public JoystickAxis getXAxis() {
+            return xAxis;
+        }
+
+        @Override
+        public JoystickAxis getYAxis() {
+            return yAxis;
+        }
+
+        @Override
+        public JoystickAxis getPovXAxis() {
+            return povX;
+        }
+
+        @Override
+        public JoystickAxis getPovYAxis() {
+            return povY;
+        }
+
+    }
+
+    public class AndroidJoystickAxis extends DefaultJoystickAxis implements SensorJoystickAxis {
+        float zeroRawValue = 0f;
+        float curRawValue = 0f;
+        float lastRawValue = 0f;
+        boolean hasChanged = false;
+        float maxRawValue = FastMath.HALF_PI;
+        boolean enabled = true;
+
+        public AndroidJoystickAxis(InputManager inputManager, Joystick parent,
+                           int axisIndex, String name, String logicalId,
+                           boolean isAnalog, boolean isRelative, float deadZone,
+                           float maxRawValue) {
+            super(inputManager, parent, axisIndex, name, logicalId, isAnalog, isRelative, deadZone);
+
+            this.maxRawValue = maxRawValue;
+        }
+
+        public float getMaxRawValue() {
+            return maxRawValue;
+        }
+
+        public void setMaxRawValue(float maxRawValue) {
+            this.maxRawValue = maxRawValue;
+        }
+
+        protected float getLastRawValue() {
+            return lastRawValue;
+        }
+        protected void setCurRawValue(float rawValue) {
+            this.curRawValue = rawValue;
+            if (Math.abs(curRawValue - lastRawValue) > getDeadZone()) {
+                hasChanged = true;
+                lastRawValue = curRawValue;
+            } else {
+                hasChanged = false;
+            }
+        }
+
+        protected float getJoystickAxisValue() {
+            return (lastRawValue-zeroRawValue) / maxRawValue;
+        }
+
+        protected boolean isChanged() {
+            return hasChanged;
+        }
+
+        public void calibrateCenter() {
+            zeroRawValue = lastRawValue;
+            logger.log(Level.FINE, "Calibrating axis {0} to {1}",
+                    new Object[]{getName(), zeroRawValue});
+        }
+
+    }
+}

+ 257 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler.java

@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.view.MotionEvent;
+import android.view.View;
+import com.jme3.input.event.InputEvent;
+import com.jme3.input.event.MouseButtonEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+import static com.jme3.input.event.TouchEvent.Type.DOWN;
+import static com.jme3.input.event.TouchEvent.Type.MOVE;
+import static com.jme3.input.event.TouchEvent.Type.UP;
+import com.jme3.math.Vector2f;
+import java.util.HashMap;
+import java.util.logging.Logger;
+
+/**
+ * AndroidTouchHandler is the base class that receives touch inputs from the 
+ * Android system and creates the TouchEvents for jME.  This class is designed
+ * to handle the base touch events for Android rev 9 (Android 2.3).  This is
+ * extended by other classes to add features that were introducted after
+ * Android rev 9.
+ * 
+ * @author iwgeric
+ */
+public class AndroidTouchHandler implements View.OnTouchListener {
+    private static final Logger logger = Logger.getLogger(AndroidTouchHandler.class.getName());
+    
+    final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
+
+    protected int numPointers = 0;
+    
+    protected AndroidInputHandler androidInput;
+    protected AndroidGestureHandler gestureHandler;
+
+    public AndroidTouchHandler(AndroidInputHandler androidInput, AndroidGestureHandler gestureHandler) {
+        this.androidInput = androidInput;
+        this.gestureHandler = gestureHandler;
+    }
+
+    public void initialize() {
+    }
+    
+    public void destroy() {
+        setView(null);
+    }
+    
+    public void setView(View view) {
+        if (view != null) {
+            view.setOnTouchListener(this);
+        } else {
+            androidInput.getView().setOnTouchListener(null);
+        }
+    }
+    
+    protected int getPointerIndex(MotionEvent event) {
+        return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+    }
+    
+    protected int getPointerId(MotionEvent event) {
+        return event.getPointerId(getPointerIndex(event));
+    }
+    
+    protected int getAction(MotionEvent event) {
+        return event.getAction() & MotionEvent.ACTION_MASK;
+    }
+    
+    /**
+     * onTouch gets called from android thread on touch events
+     */
+    public boolean onTouch(View view, MotionEvent event) {
+        if (!androidInput.isInitialized() || view != androidInput.getView()) {
+            return false;
+        }
+        
+        boolean bWasHandled = false;
+        TouchEvent touch = null;
+        //    System.out.println("native : " + event.getAction());
+        int action = getAction(event);
+        int pointerIndex = getPointerIndex(event);
+        int pointerId = getPointerId(event);
+        Vector2f lastPos = lastPositions.get(pointerId);
+        float jmeX;
+        float jmeY;
+        
+        numPointers = event.getPointerCount();
+
+        // final int historySize = event.getHistorySize();
+        //final int pointerCount = event.getPointerCount();
+        switch (getAction(event)) {
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_DOWN:
+                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
+                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
+                touch = androidInput.getFreeTouchEvent();
+                touch.set(TouchEvent.Type.DOWN, jmeX, jmeY, 0, 0);
+                touch.setPointerId(pointerId);
+                touch.setTime(event.getEventTime());
+                touch.setPressure(event.getPressure(pointerIndex));
+
+                lastPos = new Vector2f(jmeX, jmeY);
+                lastPositions.put(pointerId, lastPos);
+
+                processEvent(touch);
+
+                bWasHandled = true;
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
+                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
+                touch = androidInput.getFreeTouchEvent();
+                touch.set(TouchEvent.Type.UP, jmeX, jmeY, 0, 0);
+                touch.setPointerId(pointerId);
+                touch.setTime(event.getEventTime());
+                touch.setPressure(event.getPressure(pointerIndex));
+                lastPositions.remove(pointerId);
+
+                processEvent(touch);
+
+                bWasHandled = true;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // Convert all pointers into events
+                for (int p = 0; p < event.getPointerCount(); p++) {
+                    jmeX = androidInput.getJmeX(event.getX(p));
+                    jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(p)));
+                    lastPos = lastPositions.get(event.getPointerId(p));
+                    if (lastPos == null) {
+                        lastPos = new Vector2f(jmeX, jmeY);
+                        lastPositions.put(event.getPointerId(p), lastPos);
+                    }
+
+                    float dX = jmeX - lastPos.x;
+                    float dY = jmeY - lastPos.y;
+                    if (dX != 0 || dY != 0) {
+                        touch = androidInput.getFreeTouchEvent();
+                        touch.set(TouchEvent.Type.MOVE, jmeX, jmeY, dX, dY);
+                        touch.setPointerId(event.getPointerId(p));
+                        touch.setTime(event.getEventTime());
+                        touch.setPressure(event.getPressure(p));
+                        lastPos.set(jmeX, jmeY);
+
+                        processEvent(touch);
+
+                        bWasHandled = true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_OUTSIDE:
+                break;
+
+        }
+
+        // Try to detect gestures
+        if (gestureHandler != null) {
+            gestureHandler.detectGesture(event);
+        }
+
+        return bWasHandled;
+    }
+
+    protected void processEvent(TouchEvent event) {
+        // Add the touch event
+        androidInput.addEvent(event);
+        // MouseEvents do not support multi-touch, so only evaluate 1 finger pointer events
+        if (androidInput.isSimulateMouse() && numPointers == 1) {
+            InputEvent mouseEvent = generateMouseEvent(event);
+            if (mouseEvent != null) {
+                // Add the mouse event
+                androidInput.addEvent(mouseEvent);
+            }
+        }
+        
+    }
+
+    // TODO: Ring Buffer for mouse events?
+    protected InputEvent generateMouseEvent(TouchEvent event) {
+        InputEvent inputEvent = null;
+        int newX;
+        int newY;
+        int newDX;
+        int newDY;
+
+        if (androidInput.isMouseEventsInvertX()) {
+            newX = (int) (androidInput.invertX(event.getX()));
+            newDX = (int)event.getDeltaX() * -1;
+        } else {
+            newX = (int) event.getX();
+            newDX = (int)event.getDeltaX();
+        }
+
+        if (androidInput.isMouseEventsInvertY()) {
+            newY = (int) (androidInput.invertY(event.getY()));
+            newDY = (int)event.getDeltaY() * -1;
+        } else {
+            newY = (int) event.getY();
+            newDY = (int)event.getDeltaY();
+        }
+
+        switch (event.getType()) {
+            case DOWN:
+                // Handle mouse down event
+                inputEvent = new MouseButtonEvent(0, true, newX, newY);
+                inputEvent.setTime(event.getTime());
+                break;
+
+            case UP:
+                // Handle mouse up event
+                inputEvent = new MouseButtonEvent(0, false, newX, newY);
+                inputEvent.setTime(event.getTime());
+                break;
+
+            case HOVER_MOVE:
+            case MOVE:
+                inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, (int)event.getScaleSpan(), (int)event.getDeltaScaleSpan());
+                inputEvent.setTime(event.getTime());
+                break;
+        }
+
+        return inputEvent;
+    }
+    
+}

+ 152 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler14.java

@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.view.MotionEvent;
+import android.view.View;
+import com.jme3.input.event.TouchEvent;
+import com.jme3.math.Vector2f;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * AndroidTouchHandler14 is an extension of AndroidTouchHander that adds the
+ * Android touch event functionality between Android rev 9 (Android 2.3) and 
+ * Android rev 14 (Android 4.0).
+ * 
+ * @author iwgeric
+ */
+public class AndroidTouchHandler14 extends AndroidTouchHandler implements 
+        View.OnHoverListener {
+    private static final Logger logger = Logger.getLogger(AndroidTouchHandler14.class.getName());
+    final private HashMap<Integer, Vector2f> lastHoverPositions = new HashMap<Integer, Vector2f>();
+    
+    public AndroidTouchHandler14(AndroidInputHandler androidInput, AndroidGestureHandler gestureHandler) {
+        super(androidInput, gestureHandler);
+    }
+
+    @Override
+    public void setView(View view) {
+        if (view != null) {
+            view.setOnHoverListener(this);
+        } else {
+            androidInput.getView().setOnHoverListener(null);
+        }
+        super.setView(view);
+    }
+    
+    public boolean onHover(View view, MotionEvent event) {
+        if (view == null || view != androidInput.getView()) {
+            return false;
+        }
+        
+        boolean consumed = false;
+        int action = getAction(event);
+        int pointerId = getPointerId(event);
+        int pointerIndex = getPointerIndex(event);
+        Vector2f lastPos = lastHoverPositions.get(pointerId);
+        float jmeX;
+        float jmeY;
+        
+        numPointers = event.getPointerCount();
+        
+        logger.log(Level.INFO, "onHover pointerId: {0}, action: {1}, x: {2}, y: {3}, numPointers: {4}", 
+                new Object[]{pointerId, action, event.getX(), event.getY(), event.getPointerCount()});
+
+        TouchEvent touchEvent;
+        switch (action) {
+            case MotionEvent.ACTION_HOVER_ENTER:
+                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
+                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
+                touchEvent = androidInput.getFreeTouchEvent();
+                touchEvent.set(TouchEvent.Type.HOVER_START, jmeX, jmeY, 0, 0);
+                touchEvent.setPointerId(pointerId);
+                touchEvent.setTime(event.getEventTime());
+                touchEvent.setPressure(event.getPressure(pointerIndex));
+                
+                lastPos = new Vector2f(jmeX, jmeY);
+                lastHoverPositions.put(pointerId, lastPos);
+                
+                processEvent(touchEvent);
+                consumed = true;
+                break;
+            case MotionEvent.ACTION_HOVER_MOVE:
+                // Convert all pointers into events
+                for (int p = 0; p < event.getPointerCount(); p++) {
+                    jmeX = androidInput.getJmeX(event.getX(p));
+                    jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(p)));
+                    lastPos = lastHoverPositions.get(event.getPointerId(p));
+                    if (lastPos == null) {
+                        lastPos = new Vector2f(jmeX, jmeY);
+                        lastHoverPositions.put(event.getPointerId(p), lastPos);
+                    }
+
+                    float dX = jmeX - lastPos.x;
+                    float dY = jmeY - lastPos.y;
+                    if (dX != 0 || dY != 0) {
+                        touchEvent = androidInput.getFreeTouchEvent();
+                        touchEvent.set(TouchEvent.Type.HOVER_MOVE, jmeX, jmeY, dX, dY);
+                        touchEvent.setPointerId(event.getPointerId(p));
+                        touchEvent.setTime(event.getEventTime());
+                        touchEvent.setPressure(event.getPressure(p));
+                        lastPos.set(jmeX, jmeY);
+
+                        processEvent(touchEvent);
+
+                    }
+                }
+                consumed = true;
+                break;
+            case MotionEvent.ACTION_HOVER_EXIT:
+                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
+                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
+                touchEvent = androidInput.getFreeTouchEvent();
+                touchEvent.set(TouchEvent.Type.HOVER_END, jmeX, jmeY, 0, 0);
+                touchEvent.setPointerId(pointerId);
+                touchEvent.setTime(event.getEventTime());
+                touchEvent.setPressure(event.getPressure(pointerIndex));
+                lastHoverPositions.remove(pointerId);
+
+                processEvent(touchEvent);
+                consumed = true;
+                break;
+            default:
+                consumed = false;
+                break;
+        }
+        
+        return consumed;
+    }
+    
+}

+ 121 - 0
jme3-android/src/main/java/com/jme3/input/android/TouchEventPool.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import com.jme3.input.event.TouchEvent;
+import com.jme3.util.RingBuffer;
+import java.util.logging.Logger;
+
+/**
+ * TouchEventPool provides a RingBuffer of jME TouchEvents to help with garbage
+ * collection on Android.  Each TouchEvent is stored in the RingBuffer and is 
+ * reused if the TouchEvent has been consumed.
+ * 
+ * If a TouchEvent has not been consumed, it is placed back into the pool at the 
+ * end for later use.  If a TouchEvent has been consumed, it is reused to avoid
+ * creating lots of little objects.
+ * 
+ * If the pool is full of unconsumed events, then a new event is created and provided.
+ * 
+ * 
+ * @author iwgeric
+ */
+public class TouchEventPool {
+    private static final Logger logger = Logger.getLogger(TouchEventPool.class.getName());
+    private final RingBuffer<TouchEvent> eventPool;
+    private final int maxEvents;
+    
+    public TouchEventPool (int maxEvents) {
+        eventPool = new RingBuffer<TouchEvent>(maxEvents);
+        this.maxEvents = maxEvents;
+    } 
+
+    public void initialize() {
+        TouchEvent newEvent;
+        while (!eventPool.isEmpty()) {
+            eventPool.pop();
+        }
+        for (int i = 0; i < maxEvents; i++) {
+            newEvent = new TouchEvent();
+            newEvent.setConsumed();
+            eventPool.push(newEvent);
+        }
+    }
+    
+    public void destroy() {
+        // Clean up queues
+        while (!eventPool.isEmpty()) {
+            eventPool.pop();
+        }
+    }
+
+    /**
+     * Fetches a touch event from the reuse pool
+     *
+     * @return a usable TouchEvent
+     */
+    public TouchEvent getNextFreeEvent() {
+        TouchEvent evt = null;
+        int curSize = eventPool.size();
+        while (curSize > 0) {
+            evt = (TouchEvent)eventPool.pop();
+            if (evt.isConsumed()) {
+                break;
+            } else {
+                eventPool.push(evt);
+                evt = null;
+            }
+            curSize--;
+        }
+
+        if (evt == null) {
+            logger.warning("eventPool full of unconsumed events");
+            evt = new TouchEvent();
+        }
+        return evt;
+    }
+    
+    /**
+     * Stores the TouchEvent back in the pool for later reuse.  It is only reused
+     * if the TouchEvent has been consumed.
+     * 
+     * @param event TouchEvent to store for later use if consumed.
+     */
+    public void storeEvent(TouchEvent event) {
+        if (eventPool.size() < maxEvents) {
+            eventPool.push(event);
+        } else {
+            logger.warning("eventPool full");
+        }
+    }    
+    
+}

+ 14 - 0
jme3-android/src/main/java/com/jme3/renderer/android/Android22Workaround.java

@@ -0,0 +1,14 @@
+package com.jme3.renderer.android;
+
+import android.opengl.GLES20;
+
+public class Android22Workaround {
+    public static void glVertexAttribPointer(int location, int components, int format, boolean normalize, int stride, int offset){
+        GLES20.glVertexAttribPointer(location,
+                                     components,
+                                     format,
+                                     normalize,
+                                     stride,
+                                     offset);
+    }
+}

+ 26 - 0
jme3-android/src/main/java/com/jme3/renderer/android/AndroidGLSurfaceView.java

@@ -0,0 +1,26 @@
+package com.jme3.renderer.android;
+ 
+import android.content.Context;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import java.util.logging.Logger;
+ 
+/**
+ * <code>AndroidGLSurfaceView</code> is derived from GLSurfaceView
+ * @author iwgeric
+ *
+ */
+public class AndroidGLSurfaceView extends GLSurfaceView {
+ 
+    private final static Logger logger = Logger.getLogger(AndroidGLSurfaceView.class.getName());
+ 
+    public AndroidGLSurfaceView(Context ctx, AttributeSet attribs) {
+        super(ctx, attribs);
+    }
+ 
+    public AndroidGLSurfaceView(Context ctx) {
+        super(ctx);
+    }
+ 
+ 
+}

+ 2530 - 0
jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java

@@ -0,0 +1,2530 @@
+/*
+ * Copyright (c) 2009-2012 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.renderer.android;
+
+import android.opengl.GLES20;
+import android.os.Build;
+import com.jme3.asset.AndroidImageInfo;
+import com.jme3.light.LightList;
+import com.jme3.material.RenderState;
+import com.jme3.math.*;
+import com.jme3.renderer.*;
+import com.jme3.renderer.android.TextureUtil.AndroidGLImageFormat;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Mesh.Mode;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.VertexBuffer.Format;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.shader.Attribute;
+import com.jme3.shader.Shader;
+import com.jme3.shader.Shader.ShaderSource;
+import com.jme3.shader.Shader.ShaderType;
+import com.jme3.shader.Uniform;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.FrameBuffer.RenderBuffer;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapAxis;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.ListMap;
+import com.jme3.util.NativeObjectManager;
+import java.nio.*;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jme3tools.shader.ShaderDebug;
+
+public class OGLESShaderRenderer implements Renderer {
+
+    private static final Logger logger = Logger.getLogger(OGLESShaderRenderer.class.getName());
+    private static final boolean VALIDATE_SHADER = false;
+    private final ByteBuffer nameBuf = BufferUtils.createByteBuffer(250);
+    private final StringBuilder stringBuf = new StringBuilder(250);
+    private final IntBuffer intBuf1 = BufferUtils.createIntBuffer(1);
+    private final IntBuffer intBuf16 = BufferUtils.createIntBuffer(16);
+    private final RenderContext context = new RenderContext();
+    private final NativeObjectManager objManager = new NativeObjectManager();
+    private final EnumSet<Caps> caps = EnumSet.noneOf(Caps.class);
+    // current state
+    private Shader boundShader;
+    // initalDrawBuf and initialReadBuf are not used on ES,
+    // http://www.khronos.org/opengles/sdk/docs/man/xhtml/glBindFramebuffer.xml
+    //private int initialDrawBuf, initialReadBuf;
+    private int glslVer;
+    private int vertexTextureUnits;
+    private int fragTextureUnits;
+    private int vertexUniforms;
+    private int fragUniforms;
+    private int vertexAttribs;
+//    private int maxFBOSamples;
+    private final int maxFBOAttachs = 1; // Only 1 color attachment on ES
+    private final int maxMRTFBOAttachs = 1; // FIXME for now, not sure if > 1 is needed for ES
+    private int maxRBSize;
+    private int maxTexSize;
+    private int maxCubeTexSize;
+    private int maxVertCount;
+    private int maxTriCount;
+    private boolean tdc;
+    private FrameBuffer lastFb = null;
+    private FrameBuffer mainFbOverride = null;
+    private final Statistics statistics = new Statistics();
+    private int vpX, vpY, vpW, vpH;
+    private int clipX, clipY, clipW, clipH;
+    //private final GL10 gl;
+    private boolean powerVr = false;
+    private boolean useVBO = false;
+
+    public OGLESShaderRenderer() {
+    }
+
+    protected void updateNameBuffer() {
+        int len = stringBuf.length();
+
+        nameBuf.position(0);
+        nameBuf.limit(len);
+        for (int i = 0; i < len; i++) {
+            nameBuf.put((byte) stringBuf.charAt(i));
+        }
+
+        nameBuf.rewind();
+    }
+
+    public Statistics getStatistics() {
+        return statistics;
+    }
+
+    public EnumSet<Caps> getCaps() {
+        return caps;
+    }
+
+    private int extractVersion(String prefixStr, String versionStr) {
+        if (versionStr != null) {
+            int spaceIdx = versionStr.indexOf(" ", prefixStr.length());
+            if (spaceIdx >= 1) {
+                versionStr = versionStr.substring(prefixStr.length(), spaceIdx).trim();
+            } else {
+                versionStr = versionStr.substring(prefixStr.length()).trim();
+            }
+            //some device have ":" at the end of the version.
+            versionStr = versionStr.replaceAll("\\:", "");
+            float version = Float.parseFloat(versionStr);
+            return (int) (version * 100);
+        } else {
+            return -1;
+        }
+    }
+
+    public void initialize() {
+        logger.log(Level.FINE, "Vendor: {0}", GLES20.glGetString(GLES20.GL_VENDOR));
+        logger.log(Level.FINE, "Renderer: {0}", GLES20.glGetString(GLES20.GL_RENDERER));
+        logger.log(Level.FINE, "Version: {0}", GLES20.glGetString(GLES20.GL_VERSION));
+        logger.log(Level.FINE, "Shading Language Version: {0}", GLES20.glGetString(GLES20.GL_SHADING_LANGUAGE_VERSION));
+
+        powerVr = GLES20.glGetString(GLES20.GL_RENDERER).contains("PowerVR");
+
+        /*
+        // Fix issue in TestRenderToMemory when GL_FRONT is the main
+        // buffer being used.
+        initialDrawBuf = glGetInteger(GL_DRAW_BUFFER);
+        initialReadBuf = glGetInteger(GL_READ_BUFFER);
+
+        // XXX: This has to be GL_BACK for canvas on Mac
+        // Since initialDrawBuf is GL_FRONT for pbuffer, gotta
+        // change this value later on ...
+//        initialDrawBuf = GL_BACK;
+//        initialReadBuf = GL_BACK;
+         */
+
+        // Check OpenGL version
+        int openGlVer = extractVersion("OpenGL ES ", GLES20.glGetString(GLES20.GL_VERSION));
+        if (openGlVer == -1) {
+            glslVer = -1;
+            throw new UnsupportedOperationException("OpenGL ES 2.0+ is required for OGLESShaderRenderer!");
+        }
+
+        // Check shader language version
+        glslVer = extractVersion("OpenGL ES GLSL ES ", GLES20.glGetString(GLES20.GL_SHADING_LANGUAGE_VERSION));
+        switch (glslVer) {
+            // TODO: When new versions of OpenGL ES shader language come out,
+            // update this.
+            default:
+                caps.add(Caps.GLSL100);
+                break;
+        }
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, intBuf16);
+        vertexTextureUnits = intBuf16.get(0);
+        logger.log(Level.FINE, "VTF Units: {0}", vertexTextureUnits);
+        if (vertexTextureUnits > 0) {
+            caps.add(Caps.VertexTextureFetch);
+        }
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_IMAGE_UNITS, intBuf16);
+        fragTextureUnits = intBuf16.get(0);
+        logger.log(Level.FINE, "Texture Units: {0}", fragTextureUnits);
+
+        // Multiply vector count by 4 to get float count.
+        GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_UNIFORM_VECTORS, intBuf16);
+        vertexUniforms = intBuf16.get(0) * 4;
+        logger.log(Level.FINER, "Vertex Uniforms: {0}", vertexUniforms);
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_FRAGMENT_UNIFORM_VECTORS, intBuf16);
+        fragUniforms = intBuf16.get(0) * 4;
+        logger.log(Level.FINER, "Fragment Uniforms: {0}", fragUniforms);
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_VARYING_VECTORS, intBuf16);
+        int varyingFloats = intBuf16.get(0) * 4;
+        logger.log(Level.FINER, "Varying Floats: {0}", varyingFloats);
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, intBuf16);
+        vertexAttribs = intBuf16.get(0);
+        logger.log(Level.FINE, "Vertex Attributes: {0}", vertexAttribs);
+
+        GLES20.glGetIntegerv(GLES20.GL_SUBPIXEL_BITS, intBuf16);
+        int subpixelBits = intBuf16.get(0);
+        logger.log(Level.FINE, "Subpixel Bits: {0}", subpixelBits);
+
+//        GLES10.glGetIntegerv(GLES10.GL_MAX_ELEMENTS_VERTICES, intBuf16);
+//        maxVertCount = intBuf16.get(0);
+//        logger.log(Level.FINER, "Preferred Batch Vertex Count: {0}", maxVertCount);
+//
+//        GLES10.glGetIntegerv(GLES10.GL_MAX_ELEMENTS_INDICES, intBuf16);
+//        maxTriCount = intBuf16.get(0);
+//        logger.log(Level.FINER, "Preferred Batch Index Count: {0}", maxTriCount);
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, intBuf16);
+        maxTexSize = intBuf16.get(0);
+        logger.log(Level.FINE, "Maximum Texture Resolution: {0}", maxTexSize);
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_CUBE_MAP_TEXTURE_SIZE, intBuf16);
+        maxCubeTexSize = intBuf16.get(0);
+        logger.log(Level.FINE, "Maximum CubeMap Resolution: {0}", maxCubeTexSize);
+
+        GLES20.glGetIntegerv(GLES20.GL_MAX_RENDERBUFFER_SIZE, intBuf16);
+        maxRBSize = intBuf16.get(0);
+        logger.log(Level.FINER, "FBO RB Max Size: {0}", maxRBSize);
+
+        /*
+        if (ctxCaps.GL_ARB_color_buffer_float){
+        // XXX: Require both 16 and 32 bit float support for FloatColorBuffer.
+        if (ctxCaps.GL_ARB_half_float_pixel){
+        caps.add(Caps.FloatColorBuffer);
+        }
+        }
+
+        if (ctxCaps.GL_ARB_depth_buffer_float){
+        caps.add(Caps.FloatDepthBuffer);
+        }
+
+        if (ctxCaps.GL_ARB_draw_instanced)
+        caps.add(Caps.MeshInstancing);
+
+        if (ctxCaps.GL_ARB_texture_buffer_object)
+        caps.add(Caps.TextureBuffer);
+
+        if (ctxCaps.GL_ARB_texture_float){
+        if (ctxCaps.GL_ARB_half_float_pixel){
+        caps.add(Caps.FloatTexture);
+        }
+        }
+
+        if (ctxCaps.GL_EXT_packed_float){
+        caps.add(Caps.PackedFloatColorBuffer);
+        if (ctxCaps.GL_ARB_half_float_pixel){
+        // because textures are usually uploaded as RGB16F
+        // need half-float pixel
+        caps.add(Caps.PackedFloatTexture);
+        }
+        }
+
+        if (ctxCaps.GL_EXT_texture_array)
+        caps.add(Caps.TextureArray);
+
+        if (ctxCaps.GL_EXT_texture_shared_exponent)
+        caps.add(Caps.SharedExponentTexture);
+
+        if (ctxCaps.GL_EXT_framebuffer_object){
+        caps.add(Caps.FrameBuffer);
+
+        glGetInteger(GL_MAX_RENDERBUFFER_SIZE_EXT, intBuf16);
+        maxRBSize = intBuf16.get(0);
+        logger.log(Level.FINER, "FBO RB Max Size: {0}", maxRBSize);
+
+        glGetInteger(GL_MAX_COLOR_ATTACHMENTS_EXT, intBuf16);
+        maxFBOAttachs = intBuf16.get(0);
+        logger.log(Level.FINER, "FBO Max renderbuffers: {0}", maxFBOAttachs);
+
+        if (ctxCaps.GL_EXT_framebuffer_multisample){
+        caps.add(Caps.FrameBufferMultisample);
+
+        glGetInteger(GL_MAX_SAMPLES_EXT, intBuf16);
+        maxFBOSamples = intBuf16.get(0);
+        logger.log(Level.FINER, "FBO Max Samples: {0}", maxFBOSamples);
+        }
+
+        if (ctxCaps.GL_ARB_draw_buffers){
+        caps.add(Caps.FrameBufferMRT);
+        glGetInteger(ARBDrawBuffers.GL_MAX_DRAW_BUFFERS_ARB, intBuf16);
+        maxMRTFBOAttachs = intBuf16.get(0);
+        logger.log(Level.FINER, "FBO Max MRT renderbuffers: {0}", maxMRTFBOAttachs);
+        }
+        }
+
+        if (ctxCaps.GL_ARB_multisample){
+        glGetInteger(ARBMultisample.GL_SAMPLE_BUFFERS_ARB, intBuf16);
+        boolean available = intBuf16.get(0) != 0;
+        glGetInteger(ARBMultisample.GL_SAMPLES_ARB, intBuf16);
+        int samples = intBuf16.get(0);
+        logger.log(Level.FINER, "Samples: {0}", samples);
+        boolean enabled = glIsEnabled(ARBMultisample.GL_MULTISAMPLE_ARB);
+        if (samples > 0 && available && !enabled){
+        glEnable(ARBMultisample.GL_MULTISAMPLE_ARB);
+        }
+        }
+         */
+
+        String extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
+        logger.log(Level.FINE, "GL_EXTENSIONS: {0}", extensions);
+
+        // Get number of compressed formats available.
+        GLES20.glGetIntegerv(GLES20.GL_NUM_COMPRESSED_TEXTURE_FORMATS, intBuf16);
+        int numCompressedFormats = intBuf16.get(0);
+
+        // Allocate buffer for compressed formats.
+        IntBuffer compressedFormats = BufferUtils.createIntBuffer(numCompressedFormats);
+        GLES20.glGetIntegerv(GLES20.GL_COMPRESSED_TEXTURE_FORMATS, compressedFormats);
+
+        // Check for errors after all glGet calls.
+        RendererUtil.checkGLError();
+
+        // Print compressed formats.
+        for (int i = 0; i < numCompressedFormats; i++) {
+            logger.log(Level.FINE, "Compressed Texture Formats: {0}", compressedFormats.get(i));
+        }
+
+        TextureUtil.loadTextureFeatures(extensions);
+
+        applyRenderState(RenderState.DEFAULT);
+        GLES20.glDisable(GLES20.GL_DITHER);
+        RendererUtil.checkGLError();
+
+        useVBO = false;
+
+        // NOTE: SDK_INT is only available since 1.6,
+        // but for jME3 it doesn't matter since android versions 1.5 and below
+        // are not supported.
+        if (Build.VERSION.SDK_INT >= 9){
+            logger.log(Level.FINE, "Force-enabling VBO (Android 2.3 or higher)");
+            useVBO = true;
+        } else {
+            useVBO = false;
+        }
+
+        logger.log(Level.FINE, "Caps: {0}", caps);
+    }
+
+    /**
+     * <code>resetGLObjects</code> should be called when die GLView gets recreated to reset all GPU objects
+     */
+    public void resetGLObjects() {
+        objManager.resetObjects();
+        statistics.clearMemory();
+        boundShader = null;
+        lastFb = null;
+        context.reset();
+    }
+
+    public void cleanup() {
+        objManager.deleteAllObjects(this);
+        statistics.clearMemory();
+    }
+
+    private void checkCap(Caps cap) {
+        if (!caps.contains(cap)) {
+            throw new UnsupportedOperationException("Required capability missing: " + cap.name());
+        }
+    }
+
+    /*********************************************************************\
+    |* Render State                                                      *|
+    \*********************************************************************/
+    public void setDepthRange(float start, float end) {
+        GLES20.glDepthRangef(start, end);
+        RendererUtil.checkGLError();
+    }
+
+    public void clearBuffers(boolean color, boolean depth, boolean stencil) {
+        int bits = 0;
+        if (color) {
+            bits = GLES20.GL_COLOR_BUFFER_BIT;
+        }
+        if (depth) {
+            bits |= GLES20.GL_DEPTH_BUFFER_BIT;
+        }
+        if (stencil) {
+            bits |= GLES20.GL_STENCIL_BUFFER_BIT;
+        }
+        if (bits != 0) {
+            GLES20.glClear(bits);
+            RendererUtil.checkGLError();
+        }
+    }
+
+    public void setBackgroundColor(ColorRGBA color) {
+        GLES20.glClearColor(color.r, color.g, color.b, color.a);
+        RendererUtil.checkGLError();
+    }
+
+    public void applyRenderState(RenderState state) {
+        /*
+        if (state.isWireframe() && !context.wireframe){
+        GLES20.glPolygonMode(GLES20.GL_FRONT_AND_BACK, GLES20.GL_LINE);
+        context.wireframe = true;
+        }else if (!state.isWireframe() && context.wireframe){
+        GLES20.glPolygonMode(GLES20.GL_FRONT_AND_BACK, GLES20.GL_FILL);
+        context.wireframe = false;
+        }
+         */
+        if (state.isDepthTest() && !context.depthTestEnabled) {
+            GLES20.glEnable(GLES20.GL_DEPTH_TEST);
+            GLES20.glDepthFunc(convertTestFunction(context.depthFunc));            
+            RendererUtil.checkGLError();
+            context.depthTestEnabled = true;
+        } else if (!state.isDepthTest() && context.depthTestEnabled) {
+            GLES20.glDisable(GLES20.GL_DEPTH_TEST);
+            RendererUtil.checkGLError();
+            context.depthTestEnabled = false;
+        }
+        if (state.getDepthFunc() != context.depthFunc) {
+            GLES20.glDepthFunc(convertTestFunction(state.getDepthFunc()));
+            context.depthFunc = state.getDepthFunc();
+        }
+
+        if (state.isDepthWrite() && !context.depthWriteEnabled) {
+            GLES20.glDepthMask(true);
+            RendererUtil.checkGLError();
+            context.depthWriteEnabled = true;
+        } else if (!state.isDepthWrite() && context.depthWriteEnabled) {
+            GLES20.glDepthMask(false);
+            RendererUtil.checkGLError();
+            context.depthWriteEnabled = false;
+        }
+        if (state.isColorWrite() && !context.colorWriteEnabled) {
+            GLES20.glColorMask(true, true, true, true);
+            RendererUtil.checkGLError();
+            context.colorWriteEnabled = true;
+        } else if (!state.isColorWrite() && context.colorWriteEnabled) {
+            GLES20.glColorMask(false, false, false, false);
+            RendererUtil.checkGLError();
+            context.colorWriteEnabled = false;
+        }
+//        if (state.isPointSprite() && !context.pointSprite) {
+////            GLES20.glEnable(GLES20.GL_POINT_SPRITE);
+////            GLES20.glTexEnvi(GLES20.GL_POINT_SPRITE, GLES20.GL_COORD_REPLACE, GLES20.GL_TRUE);
+////            GLES20.glEnable(GLES20.GL_VERTEX_PROGRAM_POINT_SIZE);
+////            GLES20.glPointParameterf(GLES20.GL_POINT_SIZE_MIN, 1.0f);
+//        } else if (!state.isPointSprite() && context.pointSprite) {
+////            GLES20.glDisable(GLES20.GL_POINT_SPRITE);
+//        }
+
+        if (state.isPolyOffset()) {
+            if (!context.polyOffsetEnabled) {
+                GLES20.glEnable(GLES20.GL_POLYGON_OFFSET_FILL);
+                GLES20.glPolygonOffset(state.getPolyOffsetFactor(),
+                        state.getPolyOffsetUnits());
+                RendererUtil.checkGLError();
+
+                context.polyOffsetEnabled = true;
+                context.polyOffsetFactor = state.getPolyOffsetFactor();
+                context.polyOffsetUnits = state.getPolyOffsetUnits();
+            } else {
+                if (state.getPolyOffsetFactor() != context.polyOffsetFactor
+                        || state.getPolyOffsetUnits() != context.polyOffsetUnits) {
+                    GLES20.glPolygonOffset(state.getPolyOffsetFactor(),
+                            state.getPolyOffsetUnits());
+                    RendererUtil.checkGLError();
+
+                    context.polyOffsetFactor = state.getPolyOffsetFactor();
+                    context.polyOffsetUnits = state.getPolyOffsetUnits();
+                }
+            }
+        } else {
+            if (context.polyOffsetEnabled) {
+                GLES20.glDisable(GLES20.GL_POLYGON_OFFSET_FILL);
+                RendererUtil.checkGLError();
+
+                context.polyOffsetEnabled = false;
+                context.polyOffsetFactor = 0;
+                context.polyOffsetUnits = 0;
+            }
+        }
+        if (state.getFaceCullMode() != context.cullMode) {
+            if (state.getFaceCullMode() == RenderState.FaceCullMode.Off) {
+                GLES20.glDisable(GLES20.GL_CULL_FACE);
+                RendererUtil.checkGLError();
+            } else {
+                GLES20.glEnable(GLES20.GL_CULL_FACE);
+                RendererUtil.checkGLError();
+            }
+
+            switch (state.getFaceCullMode()) {
+                case Off:
+                    break;
+                case Back:
+                    GLES20.glCullFace(GLES20.GL_BACK);
+                    RendererUtil.checkGLError();
+                    break;
+                case Front:
+                    GLES20.glCullFace(GLES20.GL_FRONT);
+                    RendererUtil.checkGLError();
+                    break;
+                case FrontAndBack:
+                    GLES20.glCullFace(GLES20.GL_FRONT_AND_BACK);
+                    RendererUtil.checkGLError();
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unrecognized face cull mode: "
+                            + state.getFaceCullMode());
+            }
+
+            context.cullMode = state.getFaceCullMode();
+        }
+
+        if (state.getBlendMode() != context.blendMode) {
+            if (state.getBlendMode() == RenderState.BlendMode.Off) {
+                GLES20.glDisable(GLES20.GL_BLEND);
+                RendererUtil.checkGLError();
+            } else {
+                GLES20.glEnable(GLES20.GL_BLEND);
+                switch (state.getBlendMode()) {
+                    case Off:
+                        break;
+                    case Additive:
+                        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE);
+                        break;
+                    case AlphaAdditive:
+                        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE);
+                        break;
+                    case Color:
+                        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_COLOR);
+                        break;
+                    case Alpha:
+                        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+                        break;
+                    case PremultAlpha:
+                        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+                        break;
+                    case Modulate:
+                        GLES20.glBlendFunc(GLES20.GL_DST_COLOR, GLES20.GL_ZERO);
+                        break;
+                    case ModulateX2:
+                        GLES20.glBlendFunc(GLES20.GL_DST_COLOR, GLES20.GL_SRC_COLOR);
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("Unrecognized blend mode: "
+                                + state.getBlendMode());
+                }
+                RendererUtil.checkGLError();
+            }
+            context.blendMode = state.getBlendMode();
+        }
+    }
+
+    /*********************************************************************\
+    |* Camera and World transforms                                       *|
+    \*********************************************************************/
+    public void setViewPort(int x, int y, int w, int h) {
+        if (x != vpX || vpY != y || vpW != w || vpH != h) {
+            GLES20.glViewport(x, y, w, h);
+            RendererUtil.checkGLError();
+
+            vpX = x;
+            vpY = y;
+            vpW = w;
+            vpH = h;
+        }
+    }
+
+    public void setClipRect(int x, int y, int width, int height) {
+        if (!context.clipRectEnabled) {
+            GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+            RendererUtil.checkGLError();
+            context.clipRectEnabled = true;
+        }
+        if (clipX != x || clipY != y || clipW != width || clipH != height) {
+            GLES20.glScissor(x, y, width, height);
+            RendererUtil.checkGLError();
+            clipX = x;
+            clipY = y;
+            clipW = width;
+            clipH = height;
+        }
+    }
+
+    public void clearClipRect() {
+        if (context.clipRectEnabled) {
+            GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+            RendererUtil.checkGLError();
+            context.clipRectEnabled = false;
+
+            clipX = 0;
+            clipY = 0;
+            clipW = 0;
+            clipH = 0;
+        }
+    }
+
+    public void onFrame() {
+        RendererUtil.checkGLErrorForced();
+
+        objManager.deleteUnused(this);
+    }
+
+    public void setWorldMatrix(Matrix4f worldMatrix) {
+    }
+
+    public void setViewProjectionMatrices(Matrix4f viewMatrix, Matrix4f projMatrix) {
+    }
+
+    /*********************************************************************\
+    |* Shaders                                                           *|
+    \*********************************************************************/
+    protected void updateUniformLocation(Shader shader, Uniform uniform) {
+        stringBuf.setLength(0);
+        stringBuf.append(uniform.getName()).append('\0');
+        updateNameBuffer();
+        int loc = GLES20.glGetUniformLocation(shader.getId(), uniform.getName());
+        RendererUtil.checkGLError();
+
+        if (loc < 0) {
+            uniform.setLocation(-1);
+            // uniform is not declared in shader
+        } else {
+            uniform.setLocation(loc);
+        }
+    }
+
+    protected void bindProgram(Shader shader) {
+        int shaderId = shader.getId();
+        if (context.boundShaderProgram != shaderId) {
+            GLES20.glUseProgram(shaderId);
+            RendererUtil.checkGLError();
+
+            statistics.onShaderUse(shader, true);
+            boundShader = shader;
+            context.boundShaderProgram = shaderId;
+        } else {
+            statistics.onShaderUse(shader, false);
+        }
+    }
+
+    protected void updateUniform(Shader shader, Uniform uniform) {
+        int shaderId = shader.getId();
+
+        assert uniform.getName() != null;
+        assert shader.getId() > 0;
+
+        if (context.boundShaderProgram != shaderId) {
+            GLES20.glUseProgram(shaderId);
+            RendererUtil.checkGLError();
+
+            statistics.onShaderUse(shader, true);
+            boundShader = shader;
+            context.boundShaderProgram = shaderId;
+        } else {
+            statistics.onShaderUse(shader, false);
+        }
+
+        int loc = uniform.getLocation();
+        if (loc == -1) {
+            return;
+        }
+
+        if (loc == -2) {
+            // get uniform location
+            updateUniformLocation(shader, uniform);
+            if (uniform.getLocation() == -1) {
+                // not declared, ignore
+                uniform.clearUpdateNeeded();
+                return;
+            }
+            loc = uniform.getLocation();
+        }
+
+        if (uniform.getVarType() == null) {
+            // removed logging the warning to avoid flooding the log
+            // (LWJGL also doesn't post a warning)
+            //logger.log(Level.FINEST, "Uniform value is not set yet. Shader: {0}, Uniform: {1}",
+            //        new Object[]{shader.toString(), uniform.toString()});
+            return; // value not set yet..
+        }
+
+        statistics.onUniformSet();
+
+        uniform.clearUpdateNeeded();
+        FloatBuffer fb;
+        IntBuffer ib;
+        switch (uniform.getVarType()) {
+            case Float:
+                Float f = (Float) uniform.getValue();
+                GLES20.glUniform1f(loc, f.floatValue());
+                break;
+            case Vector2:
+                Vector2f v2 = (Vector2f) uniform.getValue();
+                GLES20.glUniform2f(loc, v2.getX(), v2.getY());
+                break;
+            case Vector3:
+                Vector3f v3 = (Vector3f) uniform.getValue();
+                GLES20.glUniform3f(loc, v3.getX(), v3.getY(), v3.getZ());
+                break;
+            case Vector4:
+                Object val = uniform.getValue();
+                if (val instanceof ColorRGBA) {
+                    ColorRGBA c = (ColorRGBA) val;
+                    GLES20.glUniform4f(loc, c.r, c.g, c.b, c.a);
+                } else if (val instanceof Vector4f) {
+                    Vector4f c = (Vector4f) val;
+                    GLES20.glUniform4f(loc, c.x, c.y, c.z, c.w);
+                } else {
+                    Quaternion c = (Quaternion) uniform.getValue();
+                    GLES20.glUniform4f(loc, c.getX(), c.getY(), c.getZ(), c.getW());
+                }
+                break;
+            case Boolean:
+                Boolean b = (Boolean) uniform.getValue();
+                GLES20.glUniform1i(loc, b.booleanValue() ? GLES20.GL_TRUE : GLES20.GL_FALSE);
+                break;
+            case Matrix3:
+                fb = (FloatBuffer) uniform.getValue();
+                assert fb.remaining() == 9;
+                GLES20.glUniformMatrix3fv(loc, 1, false, fb);
+                break;
+            case Matrix4:
+                fb = (FloatBuffer) uniform.getValue();
+                assert fb.remaining() == 16;
+                GLES20.glUniformMatrix4fv(loc, 1, false, fb);
+                break;
+            case IntArray:
+                ib = (IntBuffer) uniform.getValue();
+                GLES20.glUniform1iv(loc, ib.limit(), ib);
+                break;
+            case FloatArray:
+                fb = (FloatBuffer) uniform.getValue();
+                GLES20.glUniform1fv(loc, fb.limit(), fb);
+                break;
+            case Vector2Array:
+                fb = (FloatBuffer) uniform.getValue();
+                GLES20.glUniform2fv(loc, fb.limit() / 2, fb);
+                break;
+            case Vector3Array:
+                fb = (FloatBuffer) uniform.getValue();
+                GLES20.glUniform3fv(loc, fb.limit() / 3, fb);
+                break;
+            case Vector4Array:
+                fb = (FloatBuffer) uniform.getValue();
+                GLES20.glUniform4fv(loc, fb.limit() / 4, fb);
+                break;
+            case Matrix4Array:
+                fb = (FloatBuffer) uniform.getValue();
+                GLES20.glUniformMatrix4fv(loc, fb.limit() / 16, false, fb);
+                break;
+            case Int:
+                Integer i = (Integer) uniform.getValue();
+                GLES20.glUniform1i(loc, i.intValue());
+                break;
+            default:
+                throw new UnsupportedOperationException("Unsupported uniform type: " + uniform.getVarType());
+        }
+        RendererUtil.checkGLError();
+    }
+
+    protected void updateShaderUniforms(Shader shader) {
+        ListMap<String, Uniform> uniforms = shader.getUniformMap();
+        for (int i = 0; i < uniforms.size(); i++) {
+            Uniform uniform = uniforms.getValue(i);
+            if (uniform.isUpdateNeeded()) {
+                updateUniform(shader, uniform);
+            }
+        }
+    }
+
+    protected void resetUniformLocations(Shader shader) {
+        ListMap<String, Uniform> uniforms = shader.getUniformMap();
+        for (int i = 0; i < uniforms.size(); i++) {
+            Uniform uniform = uniforms.getValue(i);
+            uniform.reset(); // e.g check location again
+        }
+    }
+
+    /*
+     * (Non-javadoc)
+     * Only used for fixed-function. Ignored.
+     */
+    public void setLighting(LightList list) {
+    }
+
+    public int convertShaderType(ShaderType type) {
+        switch (type) {
+            case Fragment:
+                return GLES20.GL_FRAGMENT_SHADER;
+            case Vertex:
+                return GLES20.GL_VERTEX_SHADER;
+//            case Geometry:
+//                return ARBGeometryShader4.GL_GEOMETRY_SHADER_ARB;
+            default:
+                throw new RuntimeException("Unrecognized shader type.");
+        }
+    }
+
+    public void updateShaderSourceData(ShaderSource source) {
+        int id = source.getId();
+        if (id == -1) {
+            // Create id
+            id = GLES20.glCreateShader(convertShaderType(source.getType()));
+            RendererUtil.checkGLError();
+
+            if (id <= 0) {
+                throw new RendererException("Invalid ID received when trying to create shader.");
+            }
+            source.setId(id);
+        }
+
+        if (!source.getLanguage().equals("GLSL100")) {
+            throw new RendererException("This shader cannot run in OpenGL ES. "
+                                      + "Only GLSL 1.0 shaders are supported.");
+        }
+
+        // upload shader source
+        // merge the defines and source code
+        byte[] definesCodeData = source.getDefines().getBytes();
+        byte[] sourceCodeData = source.getSource().getBytes();
+        ByteBuffer codeBuf = BufferUtils.createByteBuffer(definesCodeData.length
+                                                        + sourceCodeData.length);
+        codeBuf.put(definesCodeData);
+        codeBuf.put(sourceCodeData);
+        codeBuf.flip();
+
+        if (powerVr && source.getType() == ShaderType.Vertex) {
+            // XXX: This is to fix a bug in old PowerVR, remove
+            // when no longer applicable.
+            GLES20.glShaderSource(
+                    id, source.getDefines()
+                    + source.getSource());
+        } else {
+            String precision ="";
+            if (source.getType() == ShaderType.Fragment) {
+                precision =  "precision mediump float;\n";
+            }
+            GLES20.glShaderSource(
+                    id,
+                    precision
+                    +source.getDefines()
+                    + source.getSource());
+        }
+//        int range[] = new int[2];
+//        int precision[] =  new int[1];
+//        GLES20.glGetShaderPrecisionFormat(GLES20.GL_VERTEX_SHADER, GLES20.GL_HIGH_FLOAT, range, 0, precision, 0);
+//        System.out.println("PRECISION HIGH FLOAT VERTEX");
+//        System.out.println("range "+range[0]+"," +range[1]);
+//        System.out.println("precision "+precision[0]);
+
+        GLES20.glCompileShader(id);
+        RendererUtil.checkGLError();
+
+        GLES20.glGetShaderiv(id, GLES20.GL_COMPILE_STATUS, intBuf1);
+        RendererUtil.checkGLError();
+
+        boolean compiledOK = intBuf1.get(0) == GLES20.GL_TRUE;
+        String infoLog = null;
+
+        if (VALIDATE_SHADER || !compiledOK) {
+            // even if compile succeeded, check
+            // log for warnings
+            GLES20.glGetShaderiv(id, GLES20.GL_INFO_LOG_LENGTH, intBuf1);
+            RendererUtil.checkGLError();
+            infoLog = GLES20.glGetShaderInfoLog(id);
+        }
+
+        if (compiledOK) {
+            if (infoLog != null) {
+                logger.log(Level.FINE, "compile success: {0}, {1}", new Object[]{source.getName(), infoLog});
+            } else {
+                logger.log(Level.FINE, "compile success: {0}", source.getName());
+            }
+            source.clearUpdateNeeded();
+        } else {
+           logger.log(Level.WARNING, "Bad compile of:\n{0}",
+                    new Object[]{ShaderDebug.formatShaderSource(source.getDefines(), source.getSource(),stringBuf.toString())});
+            if (infoLog != null) {
+                throw new RendererException("compile error in:" + source + " error:" + infoLog);
+            } else {
+                throw new RendererException("compile error in:" + source + " error: <not provided>");
+            }
+        }
+    }
+
+    public void updateShaderData(Shader shader) {
+        int id = shader.getId();
+        boolean needRegister = false;
+        if (id == -1) {
+            // create program
+            id = GLES20.glCreateProgram();
+            RendererUtil.checkGLError();
+
+            if (id <= 0) {
+                throw new RendererException("Invalid ID received when trying to create shader program.");
+            }
+
+            shader.setId(id);
+            needRegister = true;
+        }
+
+        for (ShaderSource source : shader.getSources()) {
+            if (source.isUpdateNeeded()) {
+                updateShaderSourceData(source);
+            }
+
+            GLES20.glAttachShader(id, source.getId());
+            RendererUtil.checkGLError();
+        }
+
+        // link shaders to program
+        GLES20.glLinkProgram(id);
+        RendererUtil.checkGLError();
+
+        GLES20.glGetProgramiv(id, GLES20.GL_LINK_STATUS, intBuf1);
+        RendererUtil.checkGLError();
+
+        boolean linkOK = intBuf1.get(0) == GLES20.GL_TRUE;
+        String infoLog = null;
+
+        if (VALIDATE_SHADER || !linkOK) {
+            GLES20.glGetProgramiv(id, GLES20.GL_INFO_LOG_LENGTH, intBuf1);
+            RendererUtil.checkGLError();
+
+            int length = intBuf1.get(0);
+            if (length > 3) {
+                // get infos
+                infoLog = GLES20.glGetProgramInfoLog(id);
+                RendererUtil.checkGLError();
+            }
+        }
+
+        if (linkOK) {
+            if (infoLog != null) {
+                logger.log(Level.FINE, "shader link success. \n{0}", infoLog);
+            } else {
+                logger.fine("shader link success");
+            }
+            shader.clearUpdateNeeded();
+            if (needRegister) {
+                // Register shader for clean up if it was created in this method.
+                objManager.registerObject(shader);
+                statistics.onNewShader();
+            } else {
+                // OpenGL spec: uniform locations may change after re-link
+                resetUniformLocations(shader);
+            }
+        } else {
+            if (infoLog != null) {
+                throw new RendererException("Shader link failure, shader:" + shader + " info:" + infoLog);
+            } else {
+                throw new RendererException("Shader link failure, shader:" + shader + " info: <not provided>");
+            }
+        }
+    }
+
+    public void setShader(Shader shader) {
+        if (shader == null) {
+            throw new IllegalArgumentException("Shader cannot be null");
+        } else {
+            if (shader.isUpdateNeeded()) {
+                updateShaderData(shader);
+            }
+
+            // NOTE: might want to check if any of the
+            // sources need an update?
+
+            assert shader.getId() > 0;
+
+            updateShaderUniforms(shader);
+            bindProgram(shader);
+        }
+    }
+
+    public void deleteShaderSource(ShaderSource source) {
+        if (source.getId() < 0) {
+            logger.warning("Shader source is not uploaded to GPU, cannot delete.");
+            return;
+        }
+
+        source.clearUpdateNeeded();
+
+        GLES20.glDeleteShader(source.getId());
+        RendererUtil.checkGLError();
+
+        source.resetObject();
+    }
+
+    public void deleteShader(Shader shader) {
+        if (shader.getId() == -1) {
+            logger.warning("Shader is not uploaded to GPU, cannot delete.");
+            return;
+        }
+
+        for (ShaderSource source : shader.getSources()) {
+            if (source.getId() != -1) {
+                GLES20.glDetachShader(shader.getId(), source.getId());
+                RendererUtil.checkGLError();
+
+                deleteShaderSource(source);
+            }
+        }
+
+        GLES20.glDeleteProgram(shader.getId());
+        RendererUtil.checkGLError();
+
+        statistics.onDeleteShader();
+        shader.resetObject();
+    }
+
+     private int convertTestFunction(RenderState.TestFunction testFunc) {
+        switch (testFunc) {
+            case Never:
+                return GLES20.GL_NEVER;
+            case Less:
+                return GLES20.GL_LESS;
+            case LessOrEqual:
+                return GLES20.GL_LEQUAL;
+            case Greater:
+                return GLES20.GL_GREATER;
+            case GreaterOrEqual:
+                return GLES20.GL_GEQUAL;
+            case Equal:
+                return GLES20.GL_EQUAL;
+            case NotEqual:
+                return GLES20.GL_NOTEQUAL;
+            case Always:
+                return GLES20.GL_ALWAYS;
+            default:
+                throw new UnsupportedOperationException("Unrecognized test function: " + testFunc);
+        }
+    }
+    
+    /*********************************************************************\
+    |* Framebuffers                                                      *|
+    \*********************************************************************/
+    public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst) {
+        copyFrameBuffer(src, dst, true);
+    }
+
+    public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst, boolean copyDepth) {
+            throw new RendererException("Copy framebuffer not implemented yet.");
+
+//        if (GLContext.getCapabilities().GL_EXT_framebuffer_blit) {
+//            int srcX0 = 0;
+//            int srcY0 = 0;
+//            int srcX1 = 0;
+//            int srcY1 = 0;
+//
+//            int dstX0 = 0;
+//            int dstY0 = 0;
+//            int dstX1 = 0;
+//            int dstY1 = 0;
+//
+//            int prevFBO = context.boundFBO;
+//
+//            if (mainFbOverride != null) {
+//                if (src == null) {
+//                    src = mainFbOverride;
+//                }
+//                if (dst == null) {
+//                    dst = mainFbOverride;
+//                }
+//            }
+//
+//            if (src != null && src.isUpdateNeeded()) {
+//                updateFrameBuffer(src);
+//            }
+//
+//            if (dst != null && dst.isUpdateNeeded()) {
+//                updateFrameBuffer(dst);
+//            }
+//
+//            if (src == null) {
+//                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+//                srcX0 = vpX;
+//                srcY0 = vpY;
+//                srcX1 = vpX + vpW;
+//                srcY1 = vpY + vpH;
+//            } else {
+//                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, src.getId());
+//                srcX1 = src.getWidth();
+//                srcY1 = src.getHeight();
+//            }
+//            if (dst == null) {
+//                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+//                dstX0 = vpX;
+//                dstY0 = vpY;
+//                dstX1 = vpX + vpW;
+//                dstY1 = vpY + vpH;
+//            } else {
+//                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, dst.getId());
+//                dstX1 = dst.getWidth();
+//                dstY1 = dst.getHeight();
+//            }
+//
+//
+//            int mask = GL_COLOR_BUFFER_BIT;
+//            if (copyDepth) {
+//                mask |= GL_DEPTH_BUFFER_BIT;
+//            }
+//            GLES20.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1,
+//                    dstX0, dstY0, dstX1, dstY1, mask,
+//                    GL_NEAREST);
+//
+//
+//            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, prevFBO);
+//            try {
+//                checkFrameBufferError();
+//            } catch (IllegalStateException ex) {
+//                logger.log(Level.SEVERE, "Source FBO:\n{0}", src);
+//                logger.log(Level.SEVERE, "Dest FBO:\n{0}", dst);
+//                throw ex;
+//            }
+//        } else {
+//            throw new RendererException("EXT_framebuffer_blit required.");
+//            // TODO: support non-blit copies?
+//        }
+    }
+
+    private void checkFrameBufferStatus(FrameBuffer fb) {
+        try {
+            checkFrameBufferError();
+        } catch (IllegalStateException ex) {
+            logger.log(Level.SEVERE, "=== jMonkeyEngine FBO State ===\n{0}", fb);
+            printRealFrameBufferInfo(fb);
+            throw ex;
+        }
+    }
+
+    private void checkFrameBufferError() {
+        int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
+        switch (status) {
+            case GLES20.GL_FRAMEBUFFER_COMPLETE:
+                break;
+            case GLES20.GL_FRAMEBUFFER_UNSUPPORTED:
+                //Choose different formats
+                throw new IllegalStateException("Framebuffer object format is "
+                        + "unsupported by the video hardware.");
+            case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
+                throw new IllegalStateException("Framebuffer has erronous attachment.");
+            case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
+                throw new IllegalStateException("Framebuffer doesn't have any renderbuffers attached.");
+            case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
+                throw new IllegalStateException("Framebuffer attachments must have same dimensions.");
+//            case GLES20.GL_FRAMEBUFFER_INCOMPLETE_FORMATS:
+//                throw new IllegalStateException("Framebuffer attachments must have same formats.");
+//            case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
+//                throw new IllegalStateException("Incomplete draw buffer.");
+//            case GLES20.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
+//                throw new IllegalStateException("Incomplete read buffer.");
+//            case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT:
+//                throw new IllegalStateException("Incomplete multisample buffer.");
+            default:
+                //Programming error; will fail on all hardware
+                throw new IllegalStateException("Some video driver error "
+                        + "or programming error occured. "
+                        + "Framebuffer object status is invalid: " + status);
+        }
+    }
+
+    private void printRealRenderBufferInfo(FrameBuffer fb, RenderBuffer rb, String name) {
+        System.out.println("== Renderbuffer " + name + " ==");
+        System.out.println("RB ID: " + rb.getId());
+        System.out.println("Is proper? " + GLES20.glIsRenderbuffer(rb.getId()));
+
+        int attachment = convertAttachmentSlot(rb.getSlot());
+
+        intBuf16.clear();
+        GLES20.glGetFramebufferAttachmentParameteriv(GLES20.GL_FRAMEBUFFER,
+                attachment, GLES20.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, intBuf16);
+        int type = intBuf16.get(0);
+
+        intBuf16.clear();
+        GLES20.glGetFramebufferAttachmentParameteriv(GLES20.GL_FRAMEBUFFER,
+                attachment, GLES20.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, intBuf16);
+        int rbName = intBuf16.get(0);
+
+        switch (type) {
+            case GLES20.GL_NONE:
+                System.out.println("Type: None");
+                break;
+            case GLES20.GL_TEXTURE:
+                System.out.println("Type: Texture");
+                break;
+            case GLES20.GL_RENDERBUFFER:
+                System.out.println("Type: Buffer");
+                System.out.println("RB ID: " + rbName);
+                break;
+        }
+
+
+
+    }
+
+    private void printRealFrameBufferInfo(FrameBuffer fb) {
+//        boolean doubleBuffer = GLES20.glGetBooleanv(GLES20.GL_DOUBLEBUFFER);
+        boolean doubleBuffer = false; // FIXME
+//        String drawBuf = getTargetBufferName(glGetInteger(GL_DRAW_BUFFER));
+//        String readBuf = getTargetBufferName(glGetInteger(GL_READ_BUFFER));
+
+        int fbId = fb.getId();
+        intBuf16.clear();
+//        int curDrawBinding = GLES20.glGetIntegerv(GLES20.GL_DRAW_FRAMEBUFFER_BINDING);
+//        int curReadBinding = glGetInteger(ARBFramebufferObject.GL_READ_FRAMEBUFFER_BINDING);
+
+        System.out.println("=== OpenGL FBO State ===");
+        System.out.println("Context doublebuffered? " + doubleBuffer);
+        System.out.println("FBO ID: " + fbId);
+        System.out.println("Is proper? " + GLES20.glIsFramebuffer(fbId));
+//        System.out.println("Is bound to draw? " + (fbId == curDrawBinding));
+//        System.out.println("Is bound to read? " + (fbId == curReadBinding));
+//        System.out.println("Draw buffer: " + drawBuf);
+//        System.out.println("Read buffer: " + readBuf);
+
+        if (context.boundFBO != fbId) {
+            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbId);
+            context.boundFBO = fbId;
+        }
+
+        if (fb.getDepthBuffer() != null) {
+            printRealRenderBufferInfo(fb, fb.getDepthBuffer(), "Depth");
+        }
+        for (int i = 0; i < fb.getNumColorBuffers(); i++) {
+            printRealRenderBufferInfo(fb, fb.getColorBuffer(i), "Color" + i);
+        }
+    }
+
+    private void updateRenderBuffer(FrameBuffer fb, RenderBuffer rb) {
+        int id = rb.getId();
+        if (id == -1) {
+            GLES20.glGenRenderbuffers(1, intBuf1);
+            RendererUtil.checkGLError();
+
+            id = intBuf1.get(0);
+            rb.setId(id);
+        }
+
+        if (context.boundRB != id) {
+            GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, id);
+            RendererUtil.checkGLError();
+
+            context.boundRB = id;
+        }
+
+        if (fb.getWidth() > maxRBSize || fb.getHeight() > maxRBSize) {
+            throw new RendererException("Resolution " + fb.getWidth()
+                    + ":" + fb.getHeight() + " is not supported.");
+        }
+
+        AndroidGLImageFormat imageFormat = TextureUtil.getImageFormat(rb.getFormat());
+        if (imageFormat.renderBufferStorageFormat == 0) {
+            throw new RendererException("The format '" + rb.getFormat() + "' cannot be used for renderbuffers.");
+        }
+
+//        if (fb.getSamples() > 1 && GLContext.getCapabilities().GL_EXT_framebuffer_multisample) {
+        if (fb.getSamples() > 1) {
+//            // FIXME
+            throw new RendererException("Multisample FrameBuffer is not supported yet.");
+//            int samples = fb.getSamples();
+//            if (maxFBOSamples < samples) {
+//                samples = maxFBOSamples;
+//            }
+//            glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT,
+//                    samples,
+//                    glFmt.internalFormat,
+//                    fb.getWidth(),
+//                    fb.getHeight());
+        } else {
+            GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER,
+                    imageFormat.renderBufferStorageFormat,
+                    fb.getWidth(),
+                    fb.getHeight());
+
+            RendererUtil.checkGLError();
+        }
+    }
+
+    private int convertAttachmentSlot(int attachmentSlot) {
+        // can also add support for stencil here
+        if (attachmentSlot == -100) {
+            return GLES20.GL_DEPTH_ATTACHMENT;
+        } else if (attachmentSlot == 0) {
+            return GLES20.GL_COLOR_ATTACHMENT0;
+        } else {
+            throw new UnsupportedOperationException("Android does not support multiple color attachments to an FBO");
+        }
+    }
+
+    public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) {
+        Texture tex = rb.getTexture();
+        Image image = tex.getImage();
+        if (image.isUpdateNeeded()) {
+            updateTexImageData(image, tex.getType());
+
+            // NOTE: For depth textures, sets nearest/no-mips mode
+            // Required to fix "framebuffer unsupported"
+            // for old NVIDIA drivers!
+            setupTextureParams(tex);
+        }
+
+        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,
+                convertAttachmentSlot(rb.getSlot()),
+                convertTextureType(tex.getType()),
+                image.getId(),
+                0);
+
+        RendererUtil.checkGLError();
+    }
+
+    public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) {
+        boolean needAttach;
+        if (rb.getTexture() == null) {
+            // if it hasn't been created yet, then attach is required.
+            needAttach = rb.getId() == -1;
+            updateRenderBuffer(fb, rb);
+        } else {
+            needAttach = false;
+            updateRenderTexture(fb, rb);
+        }
+        if (needAttach) {
+            GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER,
+                    convertAttachmentSlot(rb.getSlot()),
+                    GLES20.GL_RENDERBUFFER,
+                    rb.getId());
+
+            RendererUtil.checkGLError();
+        }
+    }
+
+    public void updateFrameBuffer(FrameBuffer fb) {
+        int id = fb.getId();
+        if (id == -1) {
+            intBuf1.clear();
+            // create FBO
+            GLES20.glGenFramebuffers(1, intBuf1);
+            RendererUtil.checkGLError();
+
+            id = intBuf1.get(0);
+            fb.setId(id);
+            objManager.registerObject(fb);
+
+            statistics.onNewFrameBuffer();
+        }
+
+        if (context.boundFBO != id) {
+            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, id);
+            RendererUtil.checkGLError();
+
+            // binding an FBO automatically sets draw buf to GL_COLOR_ATTACHMENT0
+            context.boundDrawBuf = 0;
+            context.boundFBO = id;
+        }
+
+        FrameBuffer.RenderBuffer depthBuf = fb.getDepthBuffer();
+        if (depthBuf != null) {
+            updateFrameBufferAttachment(fb, depthBuf);
+        }
+
+        for (int i = 0; i < fb.getNumColorBuffers(); i++) {
+            FrameBuffer.RenderBuffer colorBuf = fb.getColorBuffer(i);
+            updateFrameBufferAttachment(fb, colorBuf);
+        }
+
+        fb.clearUpdateNeeded();
+    }
+
+    public void setMainFrameBufferOverride(FrameBuffer fb){
+        mainFbOverride = fb;
+    }
+
+    public void setFrameBuffer(FrameBuffer fb) {
+        if (fb == null && mainFbOverride != null) {
+            fb = mainFbOverride;
+        }
+
+        if (lastFb == fb) {
+            if (fb == null || !fb.isUpdateNeeded()) {
+                return;
+            }
+        }
+
+        // generate mipmaps for last FB if needed
+        if (lastFb != null) {
+            for (int i = 0; i < lastFb.getNumColorBuffers(); i++) {
+                RenderBuffer rb = lastFb.getColorBuffer(i);
+                Texture tex = rb.getTexture();
+                if (tex != null
+                        && tex.getMinFilter().usesMipMapLevels()) {
+                    setTexture(0, rb.getTexture());
+
+//                    int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace());
+                    int textureType = convertTextureType(tex.getType());
+                    GLES20.glGenerateMipmap(textureType);
+                    RendererUtil.checkGLError();
+                }
+            }
+        }
+
+        if (fb == null) {
+            // unbind any fbos
+            if (context.boundFBO != 0) {
+                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+                RendererUtil.checkGLError();
+
+                statistics.onFrameBufferUse(null, true);
+
+                context.boundFBO = 0;
+            }
+
+            /*
+            // select back buffer
+            if (context.boundDrawBuf != -1) {
+                glDrawBuffer(initialDrawBuf);
+                context.boundDrawBuf = -1;
+            }
+            if (context.boundReadBuf != -1) {
+                glReadBuffer(initialReadBuf);
+                context.boundReadBuf = -1;
+            }
+             */
+
+            lastFb = null;
+        } else {
+            if (fb.getNumColorBuffers() == 0 && fb.getDepthBuffer() == null) {
+                throw new IllegalArgumentException("The framebuffer: " + fb
+                        + "\nDoesn't have any color/depth buffers");
+            }
+
+            if (fb.isUpdateNeeded()) {
+                updateFrameBuffer(fb);
+            }
+
+            if (context.boundFBO != fb.getId()) {
+                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fb.getId());
+                RendererUtil.checkGLError();
+
+                statistics.onFrameBufferUse(fb, true);
+
+                // update viewport to reflect framebuffer's resolution
+                setViewPort(0, 0, fb.getWidth(), fb.getHeight());
+
+                context.boundFBO = fb.getId();
+            } else {
+                statistics.onFrameBufferUse(fb, false);
+            }
+            if (fb.getNumColorBuffers() == 0) {
+//                // make sure to select NONE as draw buf
+//                // no color buffer attached. select NONE
+                if (context.boundDrawBuf != -2) {
+//                    glDrawBuffer(GL_NONE);
+                    context.boundDrawBuf = -2;
+                }
+                if (context.boundReadBuf != -2) {
+//                    glReadBuffer(GL_NONE);
+                    context.boundReadBuf = -2;
+                }
+            } else {
+                if (fb.getNumColorBuffers() > maxFBOAttachs) {
+                    throw new RendererException("Framebuffer has more color "
+                            + "attachments than are supported"
+                            + " by the video hardware!");
+                }
+                if (fb.isMultiTarget()) {
+                    if (fb.getNumColorBuffers() > maxMRTFBOAttachs) {
+                        throw new RendererException("Framebuffer has more"
+                                + " multi targets than are supported"
+                                + " by the video hardware!");
+                    }
+
+                    if (context.boundDrawBuf != 100 + fb.getNumColorBuffers()) {
+                        intBuf16.clear();
+                        for (int i = 0; i < fb.getNumColorBuffers(); i++) {
+                            intBuf16.put(GLES20.GL_COLOR_ATTACHMENT0 + i);
+                        }
+
+                        intBuf16.flip();
+//                        glDrawBuffers(intBuf16);
+                        context.boundDrawBuf = 100 + fb.getNumColorBuffers();
+                    }
+                } else {
+                    RenderBuffer rb = fb.getColorBuffer(fb.getTargetIndex());
+                    // select this draw buffer
+                    if (context.boundDrawBuf != rb.getSlot()) {
+                        GLES20.glActiveTexture(convertAttachmentSlot(rb.getSlot()));
+                        RendererUtil.checkGLError();
+
+                        context.boundDrawBuf = rb.getSlot();
+                    }
+                }
+            }
+
+            assert fb.getId() >= 0;
+            assert context.boundFBO == fb.getId();
+
+            lastFb = fb;
+
+            checkFrameBufferStatus(fb);
+        }
+    }
+
+    /**
+     * Reads the Color Buffer from OpenGL and stores into the ByteBuffer.
+     * Make sure to call setViewPort with the appropriate viewport size before
+     * calling readFrameBuffer.
+     * @param fb FrameBuffer
+     * @param byteBuf ByteBuffer to store the Color Buffer from OpenGL
+     */
+    public void readFrameBuffer(FrameBuffer fb, ByteBuffer byteBuf) {
+        if (fb != null) {
+            RenderBuffer rb = fb.getColorBuffer();
+            if (rb == null) {
+                throw new IllegalArgumentException("Specified framebuffer"
+                        + " does not have a colorbuffer");
+            }
+
+            setFrameBuffer(fb);
+        } else {
+            setFrameBuffer(null);
+        }
+
+        GLES20.glReadPixels(vpX, vpY, vpW, vpH, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, byteBuf);
+        RendererUtil.checkGLError();
+    }
+
+    private void deleteRenderBuffer(FrameBuffer fb, RenderBuffer rb) {
+        intBuf1.put(0, rb.getId());
+        GLES20.glDeleteRenderbuffers(1, intBuf1);
+        RendererUtil.checkGLError();
+    }
+
+    public void deleteFrameBuffer(FrameBuffer fb) {
+        if (fb.getId() != -1) {
+            if (context.boundFBO == fb.getId()) {
+                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+                RendererUtil.checkGLError();
+
+                context.boundFBO = 0;
+            }
+
+            if (fb.getDepthBuffer() != null) {
+                deleteRenderBuffer(fb, fb.getDepthBuffer());
+            }
+            if (fb.getColorBuffer() != null) {
+                deleteRenderBuffer(fb, fb.getColorBuffer());
+            }
+
+            intBuf1.put(0, fb.getId());
+            GLES20.glDeleteFramebuffers(1, intBuf1);
+            RendererUtil.checkGLError();
+
+            fb.resetObject();
+
+            statistics.onDeleteFrameBuffer();
+        }
+    }
+
+    /*********************************************************************\
+    |* Textures                                                          *|
+    \*********************************************************************/
+    private int convertTextureType(Texture.Type type) {
+        switch (type) {
+            case TwoDimensional:
+                return GLES20.GL_TEXTURE_2D;
+            //        case TwoDimensionalArray:
+            //            return EXTTextureArray.GL_TEXTURE_2D_ARRAY_EXT;
+//            case ThreeDimensional:
+            //               return GLES20.GL_TEXTURE_3D;
+            case CubeMap:
+                return GLES20.GL_TEXTURE_CUBE_MAP;
+            default:
+                throw new UnsupportedOperationException("Unknown texture type: " + type);
+        }
+    }
+
+    private int convertMagFilter(Texture.MagFilter filter) {
+        switch (filter) {
+            case Bilinear:
+                return GLES20.GL_LINEAR;
+            case Nearest:
+                return GLES20.GL_NEAREST;
+            default:
+                throw new UnsupportedOperationException("Unknown mag filter: " + filter);
+        }
+    }
+
+    private int convertMinFilter(Texture.MinFilter filter) {
+        switch (filter) {
+            case Trilinear:
+                return GLES20.GL_LINEAR_MIPMAP_LINEAR;
+            case BilinearNearestMipMap:
+                return GLES20.GL_LINEAR_MIPMAP_NEAREST;
+            case NearestLinearMipMap:
+                return GLES20.GL_NEAREST_MIPMAP_LINEAR;
+            case NearestNearestMipMap:
+                return GLES20.GL_NEAREST_MIPMAP_NEAREST;
+            case BilinearNoMipMaps:
+                return GLES20.GL_LINEAR;
+            case NearestNoMipMaps:
+                return GLES20.GL_NEAREST;
+            default:
+                throw new UnsupportedOperationException("Unknown min filter: " + filter);
+        }
+    }
+
+    private int convertWrapMode(Texture.WrapMode mode) {
+        switch (mode) {
+            case BorderClamp:
+            case Clamp:
+            case EdgeClamp:
+                return GLES20.GL_CLAMP_TO_EDGE;
+            case Repeat:
+                return GLES20.GL_REPEAT;
+            case MirroredRepeat:
+                return GLES20.GL_MIRRORED_REPEAT;
+            default:
+                throw new UnsupportedOperationException("Unknown wrap mode: " + mode);
+        }
+    }
+
+    /**
+     * <code>setupTextureParams</code> sets the OpenGL context texture parameters
+     * @param tex the Texture to set the texture parameters from
+     */
+    private void setupTextureParams(Texture tex) {
+        int target = convertTextureType(tex.getType());
+
+        // filter things
+        int minFilter = convertMinFilter(tex.getMinFilter());
+        int magFilter = convertMagFilter(tex.getMagFilter());
+
+        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_MIN_FILTER, minFilter);
+        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_MAG_FILTER, magFilter);
+        RendererUtil.checkGLError();
+
+        /*
+        if (tex.getAnisotropicFilter() > 1){
+
+        if (GLContext.getCapabilities().GL_EXT_texture_filter_anisotropic){
+        glTexParameterf(target,
+        EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT,
+        tex.getAnisotropicFilter());
+        }
+
+        }
+         */
+        // repeat modes
+
+        switch (tex.getType()) {
+            case ThreeDimensional:
+            case CubeMap: // cubemaps use 3D coords
+            // GL_TEXTURE_WRAP_R is not available in api 8
+            //GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_R, convertWrapMode(tex.getWrap(WrapAxis.R)));
+            case TwoDimensional:
+            case TwoDimensionalArray:
+                GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, convertWrapMode(tex.getWrap(WrapAxis.T)));
+
+                // fall down here is intentional..
+//          case OneDimensional:
+                GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, convertWrapMode(tex.getWrap(WrapAxis.S)));
+
+                RendererUtil.checkGLError();
+                break;
+            default:
+                throw new UnsupportedOperationException("Unknown texture type: " + tex.getType());
+        }
+
+        // R to Texture compare mode
+/*
+        if (tex.getShadowCompareMode() != Texture.ShadowCompareMode.Off){
+        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_COMPARE_MODE, GLES20.GL_COMPARE_R_TO_TEXTURE);
+        GLES20.glTexParameteri(target, GLES20.GL_DEPTH_TEXTURE_MODE, GLES20.GL_INTENSITY);
+        if (tex.getShadowCompareMode() == Texture.ShadowCompareMode.GreaterOrEqual){
+        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_COMPARE_FUNC, GLES20.GL_GEQUAL);
+        }else{
+        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_COMPARE_FUNC, GLES20.GL_LEQUAL);
+        }
+        }
+         */
+    }
+
+    /**
+     * activates and binds the texture
+     * @param img
+     * @param type
+     */
+    public void updateTexImageData(Image img, Texture.Type type) {
+        int texId = img.getId();
+        if (texId == -1) {
+            // create texture
+            GLES20.glGenTextures(1, intBuf1);
+            RendererUtil.checkGLError();
+
+            texId = intBuf1.get(0);
+            img.setId(texId);
+            objManager.registerObject(img);
+
+            statistics.onNewTexture();
+        }
+
+        // bind texture
+        int target = convertTextureType(type);
+        if (context.boundTextures[0] != img) {
+            if (context.boundTextureUnit != 0) {
+                GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+                RendererUtil.checkGLError();
+
+                context.boundTextureUnit = 0;
+            }
+
+            GLES20.glBindTexture(target, texId);
+            RendererUtil.checkGLError();
+
+            context.boundTextures[0] = img;
+        }
+
+        boolean needMips = false;
+        if (img.isGeneratedMipmapsRequired()) {
+            needMips = true;
+            img.setMipmapsGenerated(true);
+        }
+
+        if (target == GLES20.GL_TEXTURE_CUBE_MAP) {
+            // Check max texture size before upload
+            if (img.getWidth() > maxCubeTexSize || img.getHeight() > maxCubeTexSize) {
+                throw new RendererException("Cannot upload cubemap " + img + ". The maximum supported cubemap resolution is " + maxCubeTexSize);
+            }
+        } else {
+            if (img.getWidth() > maxTexSize || img.getHeight() > maxTexSize) {
+                throw new RendererException("Cannot upload texture " + img + ". The maximum supported texture resolution is " + maxTexSize);
+            }
+        }
+
+        if (target == GLES20.GL_TEXTURE_CUBE_MAP) {
+            // Upload a cube map / sky box
+            @SuppressWarnings("unchecked")
+            List<AndroidImageInfo> bmps = (List<AndroidImageInfo>) img.getEfficentData();
+            if (bmps != null) {
+                // Native android bitmap
+                if (bmps.size() != 6) {
+                    throw new UnsupportedOperationException("Invalid texture: " + img
+                            + "Cubemap textures must contain 6 data units.");
+                }
+                for (int i = 0; i < 6; i++) {
+                    TextureUtil.uploadTextureBitmap(GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, bmps.get(i).getBitmap(), needMips);
+                    bmps.get(i).notifyBitmapUploaded();
+                }
+            } else {
+                // Standard jme3 image data
+                List<ByteBuffer> data = img.getData();
+                if (data.size() != 6) {
+                    throw new UnsupportedOperationException("Invalid texture: " + img
+                            + "Cubemap textures must contain 6 data units.");
+                }
+                for (int i = 0; i < 6; i++) {
+                    TextureUtil.uploadTextureAny(img, GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, i, needMips);
+                }
+            }
+        } else {
+            TextureUtil.uploadTextureAny(img, target, 0, needMips);
+            if (img.getEfficentData() instanceof AndroidImageInfo) {
+                AndroidImageInfo info = (AndroidImageInfo) img.getEfficentData();
+                info.notifyBitmapUploaded();
+            }
+        }
+
+        img.clearUpdateNeeded();
+    }
+
+    public void setTexture(int unit, Texture tex) {
+        Image image = tex.getImage();
+        if (image.isUpdateNeeded() || (image.isGeneratedMipmapsRequired() && !image.isMipmapsGenerated()) ) {
+            updateTexImageData(image, tex.getType());
+        }
+
+        int texId = image.getId();
+        assert texId != -1;
+
+        if (texId == -1) {
+            logger.warning("error: texture image has -1 id");
+        }
+
+        Image[] textures = context.boundTextures;
+
+        int type = convertTextureType(tex.getType());
+        if (!context.textureIndexList.moveToNew(unit)) {
+//             if (context.boundTextureUnit != unit){
+//                glActiveTexture(GL_TEXTURE0 + unit);
+//                context.boundTextureUnit = unit;
+//             }
+//             glEnable(type);
+        }
+
+        if (textures[unit] != image) {
+            if (context.boundTextureUnit != unit) {
+                GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
+                context.boundTextureUnit = unit;
+            }
+
+            GLES20.glBindTexture(type, texId);
+            RendererUtil.checkGLError();
+
+            textures[unit] = image;
+
+            statistics.onTextureUse(tex.getImage(), true);
+        } else {
+            statistics.onTextureUse(tex.getImage(), false);
+        }
+
+        setupTextureParams(tex);
+    }
+
+    public void modifyTexture(Texture tex, Image pixels, int x, int y) {
+      setTexture(0, tex);
+      TextureUtil.uploadSubTexture(pixels, convertTextureType(tex.getType()), 0, x, y);
+    }
+
+    public void clearTextureUnits() {
+        IDList textureList = context.textureIndexList;
+        Image[] textures = context.boundTextures;
+        for (int i = 0; i < textureList.oldLen; i++) {
+            int idx = textureList.oldList[i];
+//            if (context.boundTextureUnit != idx){
+//                glActiveTexture(GL_TEXTURE0 + idx);
+//                context.boundTextureUnit = idx;
+//            }
+//            glDisable(convertTextureType(textures[idx].getType()));
+            textures[idx] = null;
+        }
+        context.textureIndexList.copyNewToOld();
+    }
+
+    public void deleteImage(Image image) {
+        int texId = image.getId();
+        if (texId != -1) {
+            intBuf1.put(0, texId);
+            intBuf1.position(0).limit(1);
+
+            GLES20.glDeleteTextures(1, intBuf1);
+            RendererUtil.checkGLError();
+
+            image.resetObject();
+
+            statistics.onDeleteTexture();
+        }
+    }
+
+    /*********************************************************************\
+    |* Vertex Buffers and Attributes                                     *|
+    \*********************************************************************/
+    private int convertUsage(Usage usage) {
+        switch (usage) {
+            case Static:
+                return GLES20.GL_STATIC_DRAW;
+            case Dynamic:
+                return GLES20.GL_DYNAMIC_DRAW;
+            case Stream:
+                return GLES20.GL_STREAM_DRAW;
+            default:
+                throw new RuntimeException("Unknown usage type.");
+        }
+    }
+
+    private int convertVertexBufferFormat(Format format) {
+        switch (format) {
+            case Byte:
+                return GLES20.GL_BYTE;
+            case UnsignedByte:
+                return GLES20.GL_UNSIGNED_BYTE;
+            case Short:
+                return GLES20.GL_SHORT;
+            case UnsignedShort:
+                return GLES20.GL_UNSIGNED_SHORT;
+            case Int:
+                return GLES20.GL_INT;
+            case UnsignedInt:
+                return GLES20.GL_UNSIGNED_INT;
+            /*
+            case Half:
+            return NVHalfFloat.GL_HALF_FLOAT_NV;
+            //                return ARBHalfFloatVertex.GL_HALF_FLOAT;
+             */
+            case Float:
+                return GLES20.GL_FLOAT;
+//            case Double:
+//                return GLES20.GL_DOUBLE;
+            default:
+                throw new RuntimeException("Unknown buffer format.");
+
+        }
+    }
+
+    public void updateBufferData(VertexBuffer vb) {
+        int bufId = vb.getId();
+        boolean created = false;
+        if (bufId == -1) {
+            // create buffer
+            GLES20.glGenBuffers(1, intBuf1);
+            RendererUtil.checkGLError();
+
+            bufId = intBuf1.get(0);
+            vb.setId(bufId);
+            objManager.registerObject(vb);
+
+            created = true;
+        }
+
+        // bind buffer
+        int target;
+        if (vb.getBufferType() == VertexBuffer.Type.Index) {
+            target = GLES20.GL_ELEMENT_ARRAY_BUFFER;
+            if (context.boundElementArrayVBO != bufId) {
+                GLES20.glBindBuffer(target, bufId);
+                RendererUtil.checkGLError();
+
+                context.boundElementArrayVBO = bufId;
+            }
+        } else {
+            target = GLES20.GL_ARRAY_BUFFER;
+            if (context.boundArrayVBO != bufId) {
+                GLES20.glBindBuffer(target, bufId);
+                RendererUtil.checkGLError();
+
+                context.boundArrayVBO = bufId;
+            }
+        }
+
+        int usage = convertUsage(vb.getUsage());
+        vb.getData().rewind();
+
+        if (created || vb.hasDataSizeChanged()) {
+            // upload data based on format
+            int size = vb.getData().limit() * vb.getFormat().getComponentSize();
+
+            switch (vb.getFormat()) {
+                case Byte:
+                case UnsignedByte:
+                    GLES20.glBufferData(target, size, (ByteBuffer) vb.getData(), usage);
+                    RendererUtil.checkGLError();
+                    break;
+                case Short:
+                case UnsignedShort:
+                    GLES20.glBufferData(target, size, (ShortBuffer) vb.getData(), usage);
+                    RendererUtil.checkGLError();
+                    break;
+                case Int:
+                case UnsignedInt:
+                    GLES20.glBufferData(target, size, (IntBuffer) vb.getData(), usage);
+                    RendererUtil.checkGLError();
+                    break;
+                case Float:
+                    GLES20.glBufferData(target, size, (FloatBuffer) vb.getData(), usage);
+                    RendererUtil.checkGLError();
+                    break;
+                default:
+                    throw new RuntimeException("Unknown buffer format.");
+            }
+        } else {
+            int size = vb.getData().limit() * vb.getFormat().getComponentSize();
+
+            switch (vb.getFormat()) {
+                case Byte:
+                case UnsignedByte:
+                    GLES20.glBufferSubData(target, 0, size, (ByteBuffer) vb.getData());
+                    RendererUtil.checkGLError();
+                    break;
+                case Short:
+                case UnsignedShort:
+                    GLES20.glBufferSubData(target, 0, size, (ShortBuffer) vb.getData());
+                    RendererUtil.checkGLError();
+                    break;
+                case Int:
+                case UnsignedInt:
+                    GLES20.glBufferSubData(target, 0, size, (IntBuffer) vb.getData());
+                    RendererUtil.checkGLError();
+                    break;
+                case Float:
+                    GLES20.glBufferSubData(target, 0, size, (FloatBuffer) vb.getData());
+                    RendererUtil.checkGLError();
+                    break;
+                default:
+                    throw new RuntimeException("Unknown buffer format.");
+            }
+        }
+        vb.clearUpdateNeeded();
+    }
+
+    public void deleteBuffer(VertexBuffer vb) {
+        int bufId = vb.getId();
+        if (bufId != -1) {
+            // delete buffer
+            intBuf1.put(0, bufId);
+            intBuf1.position(0).limit(1);
+
+            GLES20.glDeleteBuffers(1, intBuf1);
+            RendererUtil.checkGLError();
+
+            vb.resetObject();
+        }
+    }
+
+    public void clearVertexAttribs() {
+        IDList attribList = context.attribIndexList;
+        for (int i = 0; i < attribList.oldLen; i++) {
+            int idx = attribList.oldList[i];
+
+            GLES20.glDisableVertexAttribArray(idx);
+            RendererUtil.checkGLError();
+
+            context.boundAttribs[idx] = null;
+        }
+        context.attribIndexList.copyNewToOld();
+    }
+
+    public void setVertexAttrib(VertexBuffer vb, VertexBuffer idb) {
+        if (vb.getBufferType() == VertexBuffer.Type.Index) {
+            throw new IllegalArgumentException("Index buffers not allowed to be set to vertex attrib");
+        }
+
+        if (vb.isUpdateNeeded() && idb == null) {
+            updateBufferData(vb);
+        }
+
+        int programId = context.boundShaderProgram;
+        if (programId > 0) {
+            Attribute attrib = boundShader.getAttribute(vb.getBufferType());
+            int loc = attrib.getLocation();
+            if (loc == -1) {
+                return; // not defined
+            }
+
+            if (loc == -2) {
+//                stringBuf.setLength(0);
+//                stringBuf.append("in").append(vb.getBufferType().name()).append('\0');
+//                updateNameBuffer();
+
+                String attributeName = "in" + vb.getBufferType().name();
+                loc = GLES20.glGetAttribLocation(programId, attributeName);
+                RendererUtil.checkGLError();
+
+                // not really the name of it in the shader (inPosition\0) but
+                // the internal name of the enum (Position).
+                if (loc < 0) {
+                    attrib.setLocation(-1);
+                    return; // not available in shader.
+                } else {
+                    attrib.setLocation(loc);
+                }
+            }
+
+            VertexBuffer[] attribs = context.boundAttribs;
+            if (!context.attribIndexList.moveToNew(loc)) {
+                GLES20.glEnableVertexAttribArray(loc);
+                RendererUtil.checkGLError();
+                //System.out.println("Enabled ATTRIB IDX: "+loc);
+            }
+            if (attribs[loc] != vb) {
+                // NOTE: Use id from interleaved buffer if specified
+                int bufId = idb != null ? idb.getId() : vb.getId();
+                assert bufId != -1;
+
+                if (bufId == -1) {
+                    logger.warning("invalid buffer id");
+                }
+
+                if (context.boundArrayVBO != bufId) {
+                    GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufId);
+                    RendererUtil.checkGLError();
+
+                    context.boundArrayVBO = bufId;
+                }
+
+                vb.getData().rewind();
+
+                Android22Workaround.glVertexAttribPointer(loc,
+                                    vb.getNumComponents(),
+                                    convertVertexBufferFormat(vb.getFormat()),
+                                    vb.isNormalized(),
+                                    vb.getStride(),
+                                    0);
+
+                RendererUtil.checkGLError();
+
+                attribs[loc] = vb;
+            }
+        } else {
+            throw new IllegalStateException("Cannot render mesh without shader bound");
+        }
+    }
+
+    public void setVertexAttrib(VertexBuffer vb) {
+        setVertexAttrib(vb, null);
+    }
+
+    public void drawTriangleArray(Mesh.Mode mode, int count, int vertCount) {
+        /*        if (count > 1){
+        ARBDrawInstanced.glDrawArraysInstancedARB(convertElementMode(mode), 0,
+        vertCount, count);
+        }else{*/
+        GLES20.glDrawArrays(convertElementMode(mode), 0, vertCount);
+        RendererUtil.checkGLError();
+        /*
+        }*/
+    }
+
+    public void drawTriangleList(VertexBuffer indexBuf, Mesh mesh, int count) {
+        if (indexBuf.getBufferType() != VertexBuffer.Type.Index) {
+            throw new IllegalArgumentException("Only index buffers are allowed as triangle lists.");
+        }
+
+        if (indexBuf.isUpdateNeeded()) {
+            updateBufferData(indexBuf);
+        }
+
+        int bufId = indexBuf.getId();
+        assert bufId != -1;
+
+        if (bufId == -1) {
+            throw new RendererException("Invalid buffer ID");
+        }
+
+        if (context.boundElementArrayVBO != bufId) {
+            GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufId);
+            RendererUtil.checkGLError();
+
+            context.boundElementArrayVBO = bufId;
+        }
+
+        int vertCount = mesh.getVertexCount();
+        boolean useInstancing = count > 1 && caps.contains(Caps.MeshInstancing);
+
+        Buffer indexData = indexBuf.getData();
+
+        if (indexBuf.getFormat() == Format.UnsignedInt) {
+            throw new RendererException("OpenGL ES does not support 32-bit index buffers." +
+                                        "Split your models to avoid going over 65536 vertices.");
+        }
+
+        if (mesh.getMode() == Mode.Hybrid) {
+            int[] modeStart = mesh.getModeStart();
+            int[] elementLengths = mesh.getElementLengths();
+
+            int elMode = convertElementMode(Mode.Triangles);
+            int fmt = convertVertexBufferFormat(indexBuf.getFormat());
+            int elSize = indexBuf.getFormat().getComponentSize();
+            int listStart = modeStart[0];
+            int stripStart = modeStart[1];
+            int fanStart = modeStart[2];
+            int curOffset = 0;
+            for (int i = 0; i < elementLengths.length; i++) {
+                if (i == stripStart) {
+                    elMode = convertElementMode(Mode.TriangleStrip);
+                } else if (i == fanStart) {
+                    elMode = convertElementMode(Mode.TriangleStrip);
+                }
+                int elementLength = elementLengths[i];
+
+                if (useInstancing) {
+                    //ARBDrawInstanced.
+                    throw new IllegalArgumentException("instancing is not supported.");
+                    /*
+                    GLES20.glDrawElementsInstancedARB(elMode,
+                    elementLength,
+                    fmt,
+                    curOffset,
+                    count);
+                     */
+                } else {
+                    indexBuf.getData().position(curOffset);
+                    GLES20.glDrawElements(elMode, elementLength, fmt, indexBuf.getData());
+                    RendererUtil.checkGLError();
+                    /*
+                    glDrawRangeElements(elMode,
+                    0,
+                    vertCount,
+                    elementLength,
+                    fmt,
+                    curOffset);
+                     */
+                }
+
+                curOffset += elementLength * elSize;
+            }
+        } else {
+            if (useInstancing) {
+                throw new IllegalArgumentException("instancing is not supported.");
+                //ARBDrawInstanced.
+/*
+                GLES20.glDrawElementsInstancedARB(convertElementMode(mesh.getMode()),
+                indexBuf.getData().limit(),
+                convertVertexBufferFormat(indexBuf.getFormat()),
+                0,
+                count);
+                 */
+            } else {
+                indexData.rewind();
+                GLES20.glDrawElements(
+                        convertElementMode(mesh.getMode()),
+                        indexBuf.getData().limit(),
+                        convertVertexBufferFormat(indexBuf.getFormat()),
+                        0);
+                RendererUtil.checkGLError();
+            }
+        }
+    }
+
+    /*********************************************************************\
+    |* Render Calls                                                      *|
+    \*********************************************************************/
+    public int convertElementMode(Mesh.Mode mode) {
+        switch (mode) {
+            case Points:
+                return GLES20.GL_POINTS;
+            case Lines:
+                return GLES20.GL_LINES;
+            case LineLoop:
+                return GLES20.GL_LINE_LOOP;
+            case LineStrip:
+                return GLES20.GL_LINE_STRIP;
+            case Triangles:
+                return GLES20.GL_TRIANGLES;
+            case TriangleFan:
+                return GLES20.GL_TRIANGLE_FAN;
+            case TriangleStrip:
+                return GLES20.GL_TRIANGLE_STRIP;
+            default:
+                throw new UnsupportedOperationException("Unrecognized mesh mode: " + mode);
+        }
+    }
+
+    public void updateVertexArray(Mesh mesh) {
+        logger.log(Level.FINE, "updateVertexArray({0})", mesh);
+        int id = mesh.getId();
+        /*
+        if (id == -1){
+        IntBuffer temp = intBuf1;
+        //      ARBVertexArrayObject.glGenVertexArrays(temp);
+        GLES20.glGenVertexArrays(temp);
+        id = temp.get(0);
+        mesh.setId(id);
+        }
+
+        if (context.boundVertexArray != id){
+        //     ARBVertexArrayObject.glBindVertexArray(id);
+        GLES20.glBindVertexArray(id);
+        context.boundVertexArray = id;
+        }
+         */
+        VertexBuffer interleavedData = mesh.getBuffer(Type.InterleavedData);
+        if (interleavedData != null && interleavedData.isUpdateNeeded()) {
+            updateBufferData(interleavedData);
+        }
+
+
+        for (VertexBuffer vb : mesh.getBufferList().getArray()){
+
+            if (vb.getBufferType() == Type.InterleavedData
+                    || vb.getUsage() == Usage.CpuOnly // ignore cpu-only buffers
+                    || vb.getBufferType() == Type.Index) {
+                continue;
+            }
+
+            if (vb.getStride() == 0) {
+                // not interleaved
+                setVertexAttrib(vb);
+            } else {
+                // interleaved
+                setVertexAttrib(vb, interleavedData);
+            }
+        }
+    }
+
+    /**
+     * renderMeshVertexArray renders a mesh using vertex arrays
+     */
+    private void renderMeshVertexArray(Mesh mesh, int lod, int count) {
+         for (VertexBuffer vb : mesh.getBufferList().getArray()) {
+            if (vb.getBufferType() == Type.InterleavedData
+                    || vb.getUsage() == Usage.CpuOnly // ignore cpu-only buffers
+                    || vb.getBufferType() == Type.Index) {
+                continue;
+            }
+
+            if (vb.getStride() == 0) {
+                // not interleaved
+                setVertexAttrib_Array(vb);
+            } else {
+                // interleaved
+                VertexBuffer interleavedData = mesh.getBuffer(Type.InterleavedData);
+                setVertexAttrib_Array(vb, interleavedData);
+            }
+        }
+
+        VertexBuffer indices = null;
+        if (mesh.getNumLodLevels() > 0) {
+            indices = mesh.getLodLevel(lod);
+        } else {
+            indices = mesh.getBuffer(Type.Index);//buffers.get(Type.Index.ordinal());
+        }
+        if (indices != null) {
+            drawTriangleList_Array(indices, mesh, count);
+        } else {
+            GLES20.glDrawArrays(convertElementMode(mesh.getMode()), 0, mesh.getVertexCount());
+            RendererUtil.checkGLError();
+        }
+        clearVertexAttribs();
+        clearTextureUnits();
+    }
+
+    private void renderMeshDefault(Mesh mesh, int lod, int count) {
+        VertexBuffer indices = null;
+        VertexBuffer interleavedData = mesh.getBuffer(Type.InterleavedData);
+        if (interleavedData != null && interleavedData.isUpdateNeeded()) {
+            updateBufferData(interleavedData);
+        }
+
+        //IntMap<VertexBuffer> buffers = mesh.getBuffers();     ;
+        if (mesh.getNumLodLevels() > 0) {
+            indices = mesh.getLodLevel(lod);
+        } else {
+            indices = mesh.getBuffer(Type.Index);// buffers.get(Type.Index.ordinal());
+        }
+        for (VertexBuffer vb : mesh.getBufferList().getArray()){
+            if (vb.getBufferType() == Type.InterleavedData
+                    || vb.getUsage() == Usage.CpuOnly // ignore cpu-only buffers
+                    || vb.getBufferType() == Type.Index) {
+                continue;
+            }
+
+            if (vb.getStride() == 0) {
+                // not interleaved
+                setVertexAttrib(vb);
+            } else {
+                // interleaved
+                setVertexAttrib(vb, interleavedData);
+            }
+        }
+        if (indices != null) {
+            drawTriangleList(indices, mesh, count);
+        } else {
+//            throw new UnsupportedOperationException("Cannot render without index buffer");
+            GLES20.glDrawArrays(convertElementMode(mesh.getMode()), 0, mesh.getVertexCount());
+            RendererUtil.checkGLError();
+        }
+        clearVertexAttribs();
+        clearTextureUnits();
+    }
+
+    public void renderMesh(Mesh mesh, int lod, int count) {
+        if (mesh.getVertexCount() == 0) {
+            return;
+        }
+
+        /*
+         * NOTE: not supported in OpenGL ES 2.0.
+        if (context.pointSize != mesh.getPointSize()) {
+            GLES10.glPointSize(mesh.getPointSize());
+            context.pointSize = mesh.getPointSize();
+        }
+        */
+        if (context.lineWidth != mesh.getLineWidth()) {
+            GLES20.glLineWidth(mesh.getLineWidth());
+            RendererUtil.checkGLError();
+            context.lineWidth = mesh.getLineWidth();
+        }
+
+        statistics.onMeshDrawn(mesh, lod);
+//        if (GLContext.getCapabilities().GL_ARB_vertex_array_object){
+//            renderMeshVertexArray(mesh, lod, count);
+//        }else{
+
+        if (useVBO) {
+            renderMeshDefault(mesh, lod, count);
+        } else {
+            renderMeshVertexArray(mesh, lod, count);
+        }
+    }
+
+    /**
+     * drawTriangleList_Array uses Vertex Array
+     * @param indexBuf
+     * @param mesh
+     * @param count
+     */
+    public void drawTriangleList_Array(VertexBuffer indexBuf, Mesh mesh, int count) {
+        if (indexBuf.getBufferType() != VertexBuffer.Type.Index) {
+            throw new IllegalArgumentException("Only index buffers are allowed as triangle lists.");
+        }
+
+        boolean useInstancing = count > 1 && caps.contains(Caps.MeshInstancing);
+        if (useInstancing) {
+            throw new IllegalArgumentException("Caps.MeshInstancing is not supported.");
+        }
+
+        int vertCount = mesh.getVertexCount();
+        Buffer indexData = indexBuf.getData();
+        indexData.rewind();
+
+        if (mesh.getMode() == Mode.Hybrid) {
+            int[] modeStart = mesh.getModeStart();
+            int[] elementLengths = mesh.getElementLengths();
+
+            int elMode = convertElementMode(Mode.Triangles);
+            int fmt = convertVertexBufferFormat(indexBuf.getFormat());
+            int elSize = indexBuf.getFormat().getComponentSize();
+            int listStart = modeStart[0];
+            int stripStart = modeStart[1];
+            int fanStart = modeStart[2];
+            int curOffset = 0;
+            for (int i = 0; i < elementLengths.length; i++) {
+                if (i == stripStart) {
+                    elMode = convertElementMode(Mode.TriangleStrip);
+                } else if (i == fanStart) {
+                    elMode = convertElementMode(Mode.TriangleFan);
+                }
+                int elementLength = elementLengths[i];
+
+                indexBuf.getData().position(curOffset);
+                GLES20.glDrawElements(elMode, elementLength, fmt, indexBuf.getData());
+                RendererUtil.checkGLError();
+
+                curOffset += elementLength * elSize;
+            }
+        } else {
+            GLES20.glDrawElements(
+                    convertElementMode(mesh.getMode()),
+                    indexBuf.getData().limit(),
+                    convertVertexBufferFormat(indexBuf.getFormat()),
+                    indexBuf.getData());
+            RendererUtil.checkGLError();
+        }
+    }
+
+    /**
+     * setVertexAttrib_Array uses Vertex Array
+     * @param vb
+     * @param idb
+     */
+    public void setVertexAttrib_Array(VertexBuffer vb, VertexBuffer idb) {
+        if (vb.getBufferType() == VertexBuffer.Type.Index) {
+            throw new IllegalArgumentException("Index buffers not allowed to be set to vertex attrib");
+        }
+
+        // Get shader
+        int programId = context.boundShaderProgram;
+        if (programId > 0) {
+            VertexBuffer[] attribs = context.boundAttribs;
+
+            Attribute attrib = boundShader.getAttribute(vb.getBufferType());
+            int loc = attrib.getLocation();
+            if (loc == -1) {
+                //throw new IllegalArgumentException("Location is invalid for attrib: [" + vb.getBufferType().name() + "]");
+                return;
+            } else if (loc == -2) {
+                String attributeName = "in" + vb.getBufferType().name();
+
+                loc = GLES20.glGetAttribLocation(programId, attributeName);
+                RendererUtil.checkGLError();
+
+                if (loc < 0) {
+                    attrib.setLocation(-1);
+                    return; // not available in shader.
+                } else {
+                    attrib.setLocation(loc);
+                }
+
+            }  // if (loc == -2)
+
+            if ((attribs[loc] != vb) || vb.isUpdateNeeded()) {
+                // NOTE: Use data from interleaved buffer if specified
+                VertexBuffer avb = idb != null ? idb : vb;
+                avb.getData().rewind();
+                avb.getData().position(vb.getOffset());
+
+                // Upload attribute data
+                GLES20.glVertexAttribPointer(loc,
+                        vb.getNumComponents(),
+                        convertVertexBufferFormat(vb.getFormat()),
+                        vb.isNormalized(),
+                        vb.getStride(),
+                        avb.getData());
+
+                RendererUtil.checkGLError();
+
+                GLES20.glEnableVertexAttribArray(loc);
+                RendererUtil.checkGLError();
+
+                attribs[loc] = vb;
+            } // if (attribs[loc] != vb)
+        } else {
+            throw new IllegalStateException("Cannot render mesh without shader bound");
+        }
+    }
+
+    /**
+     * setVertexAttrib_Array uses Vertex Array
+     * @param vb
+     */
+    public void setVertexAttrib_Array(VertexBuffer vb) {
+        setVertexAttrib_Array(vb, null);
+    }
+
+    public void setAlphaToCoverage(boolean value) {
+        if (value) {
+            GLES20.glEnable(GLES20.GL_SAMPLE_ALPHA_TO_COVERAGE);
+            RendererUtil.checkGLError();
+        } else {
+            GLES20.glDisable(GLES20.GL_SAMPLE_ALPHA_TO_COVERAGE);
+            RendererUtil.checkGLError();
+        }
+    }
+
+    @Override
+    public void invalidateState() {
+        context.reset();
+        boundShader = null;
+        lastFb = null;
+    }
+}

+ 129 - 0
jme3-android/src/main/java/com/jme3/renderer/android/RendererUtil.java

@@ -0,0 +1,129 @@
+package com.jme3.renderer.android;
+
+import android.opengl.GLES20;
+import android.opengl.GLU;
+import com.jme3.renderer.RendererException;
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGL11;
+
+/**
+ * Utility class used by the {@link OGLESShaderRenderer renderer} and sister
+ * classes.
+ *
+ * @author Kirill Vainer
+ */
+public class RendererUtil {
+
+    /**
+     * When set to true, every OpenGL call will check for errors and throw an
+     * exception if there is one, if false, no error checking is performed.
+     */
+    public static boolean ENABLE_ERROR_CHECKING = true;
+
+    /**
+     * Checks for an OpenGL error and throws a {@link RendererException} if
+     * there is one. Ignores the value of
+     * {@link RendererUtil#ENABLE_ERROR_CHECKING}.
+     */
+    public static void checkGLErrorForced() {
+        int error = GLES20.glGetError();
+        if (error != 0) {
+            String message = GLU.gluErrorString(error);
+            if (message == null) {
+                throw new RendererException("An unknown OpenGL error has occurred.");
+            } else {
+                throw new RendererException("An OpenGL error has occurred: " + message);
+            }
+        }
+    }
+
+    /**
+     * Checks for an EGL error and throws a {@link RendererException} if there
+     * is one. Ignores the value of {@link RendererUtil#ENABLE_ERROR_CHECKING}.
+     */
+    public static void checkEGLError(EGL10 egl) {
+        int error = egl.eglGetError();
+        if (error != EGL10.EGL_SUCCESS) {
+            String errorMessage;
+            switch (error) {
+                case EGL10.EGL_SUCCESS:
+                    return;
+                case EGL10.EGL_NOT_INITIALIZED:
+                    errorMessage = "EGL is not initialized, or could not be "
+                            + "initialized, for the specified EGL display connection. ";
+                    break;
+                case EGL10.EGL_BAD_ACCESS:
+                    errorMessage = "EGL cannot access a requested resource "
+                            + "(for example a context is bound in another thread). ";
+                    break;
+                case EGL10.EGL_BAD_ALLOC:
+                    errorMessage = "EGL failed to allocate resources for the requested operation.";
+                    break;
+                case EGL10.EGL_BAD_ATTRIBUTE:
+                    errorMessage = "An unrecognized attribute or attribute "
+                            + "value was passed in the attribute list. ";
+                    break;
+                case EGL10.EGL_BAD_CONTEXT:
+                    errorMessage = "An EGLContext argument does not name a valid EGL rendering context. ";
+                    break;
+                case EGL10.EGL_BAD_CONFIG:
+                    errorMessage = "An EGLConfig argument does not name a valid EGL frame buffer configuration. ";
+                    break;
+                case EGL10.EGL_BAD_CURRENT_SURFACE:
+                    errorMessage = "The current surface of the calling thread "
+                            + "is a window, pixel buffer or pixmap that is no longer valid. ";
+                    break;
+                case EGL10.EGL_BAD_DISPLAY:
+                    errorMessage = "An EGLDisplay argument does not name a valid EGL display connection. ";
+                    break;
+                case EGL10.EGL_BAD_SURFACE:
+                    errorMessage = "An EGLSurface argument does not name a "
+                            + "valid surface (window, pixel buffer or pixmap) configured for GL rendering. ";
+                    break;
+                case EGL10.EGL_BAD_MATCH:
+                    errorMessage = "Arguments are inconsistent (for example, a "
+                            + "valid context requires buffers not supplied by a valid surface). ";
+                    break;
+                case EGL10.EGL_BAD_PARAMETER:
+                    errorMessage = "One or more argument values are invalid.";
+                    break;
+                case EGL10.EGL_BAD_NATIVE_PIXMAP:
+                    errorMessage = "A NativePixmapType argument does not refer to a valid native pixmap. ";
+                    break;
+                case EGL10.EGL_BAD_NATIVE_WINDOW:
+                    errorMessage = "A NativeWindowType argument does not refer to a valid native window. ";
+                    break;
+                case EGL11.EGL_CONTEXT_LOST:
+                    errorMessage = "A power management event has occurred. "
+                            + "The application must destroy all contexts and reinitialise "
+                            + "OpenGL ES state and objects to continue rendering. ";
+                    break;
+                default:
+                    errorMessage = "Unknown";
+            }
+            
+            throw new RendererException("EGL error 0x" + Integer.toHexString(error) + ": " + errorMessage);
+        }
+    }
+
+    /**
+     * Checks for an OpenGL error and throws a {@link RendererException} if
+     * there is one. Does nothing if {@link RendererUtil#ENABLE_ERROR_CHECKING}
+     * is set to
+     * <code>false</code>.
+     */
+    public static void checkGLError() {
+        if (!ENABLE_ERROR_CHECKING) {
+            return;
+        }
+        int error = GLES20.glGetError();
+        if (error != 0) {
+            String message = GLU.gluErrorString(error);
+            if (message == null) {
+                throw new RendererException("An unknown OpenGL error has occurred.");
+            } else {
+                throw new RendererException("An OpenGL error has occurred: " + message);
+            }
+        }
+    }
+}

+ 571 - 0
jme3-android/src/main/java/com/jme3/renderer/android/TextureUtil.java

@@ -0,0 +1,571 @@
+package com.jme3.renderer.android;
+
+import android.graphics.Bitmap;
+import android.opengl.ETC1;
+import android.opengl.ETC1Util.ETC1Texture;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import com.jme3.asset.AndroidImageInfo;
+import com.jme3.math.FastMath;
+import com.jme3.renderer.RendererException;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.util.BufferUtils;
+import java.nio.ByteBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class TextureUtil {
+
+    private static final Logger logger = Logger.getLogger(TextureUtil.class.getName());
+    //TODO Make this configurable through appSettings
+    public static boolean ENABLE_COMPRESSION = true;
+    private static boolean NPOT = false;
+    private static boolean ETC1support = false;
+    private static boolean DXT1 = false;
+    private static boolean DEPTH24_STENCIL8 = false;
+    private static boolean DEPTH_TEXTURE = false;
+    private static boolean RGBA8 = false;
+    
+    // Same constant used by both GL_ARM_rgba8 and GL_OES_rgb8_rgba8.
+    private static final int GL_RGBA8 = 0x8058;
+    
+    private static final int GL_DXT1 = 0x83F0;
+    private static final int GL_DXT1A = 0x83F1;
+    
+    private static final int GL_DEPTH_STENCIL_OES = 0x84F9;
+    private static final int GL_UNSIGNED_INT_24_8_OES = 0x84FA;
+    private static final int GL_DEPTH24_STENCIL8_OES = 0x88F0;
+
+    public static void loadTextureFeatures(String extensionString) {
+        ETC1support = extensionString.contains("GL_OES_compressed_ETC1_RGB8_texture");
+        DEPTH24_STENCIL8 = extensionString.contains("GL_OES_packed_depth_stencil");
+        NPOT = extensionString.contains("GL_IMG_texture_npot") 
+                || extensionString.contains("GL_OES_texture_npot") 
+                || extensionString.contains("GL_NV_texture_npot_2D_mipmap");
+        
+        DXT1 = extensionString.contains("GL_EXT_texture_compression_dxt1");
+        DEPTH_TEXTURE = extensionString.contains("GL_OES_depth_texture");
+        
+        RGBA8 = extensionString.contains("GL_ARM_rgba8") ||
+                extensionString.contains("GL_OES_rgb8_rgba8");
+        
+        logger.log(Level.FINE, "Supports ETC1? {0}", ETC1support);
+        logger.log(Level.FINE, "Supports DEPTH24_STENCIL8? {0}", DEPTH24_STENCIL8);
+        logger.log(Level.FINE, "Supports NPOT? {0}", NPOT);
+        logger.log(Level.FINE, "Supports DXT1? {0}", DXT1);
+        logger.log(Level.FINE, "Supports DEPTH_TEXTURE? {0}", DEPTH_TEXTURE);
+        logger.log(Level.FINE, "Supports RGBA8? {0}", RGBA8);
+    }
+
+    private static void buildMipmap(Bitmap bitmap, boolean compress) {
+        int level = 0;
+        int height = bitmap.getHeight();
+        int width = bitmap.getWidth();
+
+        logger.log(Level.FINEST, " - Generating mipmaps for bitmap using SOFTWARE");
+
+        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
+
+        while (height >= 1 || width >= 1) {
+            //First of all, generate the texture from our bitmap and set it to the according level
+            if (compress) {
+                logger.log(Level.FINEST, " - Uploading LOD level {0} ({1}x{2}) with compression.", new Object[]{level, width, height});
+                uploadBitmapAsCompressed(GLES20.GL_TEXTURE_2D, level, bitmap, false, 0, 0);
+            } else {
+                logger.log(Level.FINEST, " - Uploading LOD level {0} ({1}x{2}) directly.", new Object[]{level, width, height});
+                GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, level, bitmap, 0);
+            }
+
+            if (height == 1 || width == 1) {
+                break;
+            }
+
+            //Increase the mipmap level
+            height /= 2;
+            width /= 2;
+            Bitmap bitmap2 = Bitmap.createScaledBitmap(bitmap, width, height, true);
+
+            // Recycle any bitmaps created as a result of scaling the bitmap.
+            // Do not recycle the original image (mipmap level 0)
+            if (level != 0) {
+                bitmap.recycle();
+            }
+
+            bitmap = bitmap2;
+
+            level++;
+        }
+    }
+
+    private static void uploadBitmapAsCompressed(int target, int level, Bitmap bitmap, boolean subTexture, int x, int y) {
+        if (bitmap.hasAlpha()) {
+            logger.log(Level.FINEST, " - Uploading bitmap directly. Cannot compress as alpha present.");
+            if (subTexture) {
+                GLUtils.texSubImage2D(target, level, x, y, bitmap);
+                RendererUtil.checkGLError();
+            } else {
+                GLUtils.texImage2D(target, level, bitmap, 0);
+                RendererUtil.checkGLError();
+            }
+        } else {
+            // Convert to RGB565
+            int bytesPerPixel = 2;
+            Bitmap rgb565 = bitmap.copy(Bitmap.Config.RGB_565, true);
+
+            // Put texture data into ByteBuffer
+            ByteBuffer inputImage = BufferUtils.createByteBuffer(bitmap.getRowBytes() * bitmap.getHeight());
+            rgb565.copyPixelsToBuffer(inputImage);
+            inputImage.position(0);
+
+            // Delete the copied RGB565 image
+            rgb565.recycle();
+
+            // Encode the image into the output bytebuffer
+            int encodedImageSize = ETC1.getEncodedDataSize(bitmap.getWidth(), bitmap.getHeight());
+            ByteBuffer compressedImage = BufferUtils.createByteBuffer(encodedImageSize);
+            ETC1.encodeImage(inputImage, bitmap.getWidth(),
+                    bitmap.getHeight(),
+                    bytesPerPixel,
+                    bytesPerPixel * bitmap.getWidth(),
+                    compressedImage);
+
+            // Delete the input image buffer
+            BufferUtils.destroyDirectBuffer(inputImage);
+
+            // Create an ETC1Texture from the compressed image data
+            ETC1Texture etc1tex = new ETC1Texture(bitmap.getWidth(), bitmap.getHeight(), compressedImage);
+
+            // Upload the ETC1Texture
+            if (bytesPerPixel == 2) {
+                int oldSize = (bitmap.getRowBytes() * bitmap.getHeight());
+                int newSize = compressedImage.capacity();
+                logger.log(Level.FINEST, " - Uploading compressed image to GL, oldSize = {0}, newSize = {1}, ratio = {2}", new Object[]{oldSize, newSize, (float) oldSize / newSize});
+                if (subTexture) {
+                    GLES20.glCompressedTexSubImage2D(target,
+                            level,
+                            x, y,
+                            bitmap.getWidth(),
+                            bitmap.getHeight(),
+                            ETC1.ETC1_RGB8_OES,
+                            etc1tex.getData().capacity(),
+                            etc1tex.getData());
+                    
+                    RendererUtil.checkGLError();
+                } else {
+                    GLES20.glCompressedTexImage2D(target,
+                            level,
+                            ETC1.ETC1_RGB8_OES,
+                            bitmap.getWidth(),
+                            bitmap.getHeight(),
+                            0,
+                            etc1tex.getData().capacity(),
+                            etc1tex.getData());
+                    
+                    RendererUtil.checkGLError();
+                }
+
+//                ETC1Util.loadTexture(target, level, 0, GLES20.GL_RGB,
+//                        GLES20.GL_UNSIGNED_SHORT_5_6_5, etc1Texture);
+//            } else if (bytesPerPixel == 3) {
+//                ETC1Util.loadTexture(target, level, 0, GLES20.GL_RGB,
+//                        GLES20.GL_UNSIGNED_BYTE, etc1Texture);
+            }
+
+            BufferUtils.destroyDirectBuffer(compressedImage);
+        }
+    }
+
+    /**
+     * <code>uploadTextureBitmap</code> uploads a native android bitmap
+     */
+    public static void uploadTextureBitmap(final int target, Bitmap bitmap, boolean needMips) {
+        uploadTextureBitmap(target, bitmap, needMips, false, 0, 0);
+    }
+
+    /**
+     * <code>uploadTextureBitmap</code> uploads a native android bitmap
+     */
+    public static void uploadTextureBitmap(final int target, Bitmap bitmap, boolean needMips, boolean subTexture, int x, int y) {
+        boolean recycleBitmap = false;
+        //TODO, maybe this should raise an exception when NPOT is not supported
+
+        boolean willCompress = ENABLE_COMPRESSION && ETC1support && !bitmap.hasAlpha();
+        if (needMips && willCompress) {
+            // Image is compressed and mipmaps are desired, generate them
+            // using software.
+            buildMipmap(bitmap, willCompress);
+        } else {
+            if (willCompress) {
+                // Image is compressed but mipmaps are not desired, upload directly.
+                logger.log(Level.FINEST, " - Uploading compressed bitmap. Mipmaps are not generated.");
+                uploadBitmapAsCompressed(target, 0, bitmap, subTexture, x, y);
+
+            } else {
+                // Image is not compressed, mipmaps may or may not be desired.
+                logger.log(Level.FINEST, " - Uploading bitmap directly.{0}",
+                        (needMips
+                        ? " Mipmaps will be generated in HARDWARE"
+                        : " Mipmaps are not generated."));
+                if (subTexture) {
+                    System.err.println("x : " + x + " y :" + y + " , " + bitmap.getWidth() + "/" + bitmap.getHeight());
+                    GLUtils.texSubImage2D(target, 0, x, y, bitmap);
+                    RendererUtil.checkGLError();
+                } else {
+                    GLUtils.texImage2D(target, 0, bitmap, 0);
+                    RendererUtil.checkGLError();
+                }
+
+                if (needMips) {
+                    // No pregenerated mips available,
+                    // generate from base level if required
+                    GLES20.glGenerateMipmap(target);
+                    RendererUtil.checkGLError();
+                }
+            }
+        }
+
+        if (recycleBitmap) {
+            bitmap.recycle();
+        }
+    }
+
+    public static void uploadTextureAny(Image img, int target, int index, boolean needMips) {
+        if (img.getEfficentData() instanceof AndroidImageInfo) {
+            logger.log(Level.FINEST, " === Uploading image {0}. Using BITMAP PATH === ", img);
+            // If image was loaded from asset manager, use fast path
+            AndroidImageInfo imageInfo = (AndroidImageInfo) img.getEfficentData();
+            uploadTextureBitmap(target, imageInfo.getBitmap(), needMips);
+        } else {
+            logger.log(Level.FINEST, " === Uploading image {0}. Using BUFFER PATH === ", img);
+            boolean wantGeneratedMips = needMips && !img.hasMipmaps();
+            if (wantGeneratedMips && img.getFormat().isCompressed()) {
+                logger.log(Level.WARNING, "Generating mipmaps is only"
+                        + " supported for Bitmap based or non-compressed images!");
+            }
+
+            // Upload using slower path
+            logger.log(Level.FINEST, " - Uploading bitmap directly.{0}",
+                    (wantGeneratedMips
+                    ? " Mipmaps will be generated in HARDWARE"
+                    : " Mipmaps are not generated."));
+            
+            uploadTexture(img, target, index);
+
+            // Image was uploaded using slower path, since it is not compressed,
+            // then compress it
+            if (wantGeneratedMips) {
+                // No pregenerated mips available,
+                // generate from base level if required
+                GLES20.glGenerateMipmap(target);
+            }
+        }
+    }
+
+    private static void unsupportedFormat(Format fmt) {
+        throw new UnsupportedOperationException("The image format '" + fmt + "' is unsupported by the video hardware.");
+    }
+
+    public static AndroidGLImageFormat getImageFormat(Format fmt) throws UnsupportedOperationException {
+        AndroidGLImageFormat imageFormat = new AndroidGLImageFormat();
+        switch (fmt) {
+            case RGBA16:
+            case RGB16:
+            case RGB10:
+            case Luminance16:
+            case Luminance16Alpha16:
+            case Alpha16:
+            case Depth32:
+            case Depth32F:
+                throw new UnsupportedOperationException("The image format '"
+                        + fmt + "' is not supported by OpenGL ES 2.0 specification.");
+            case Alpha8:
+                imageFormat.format = GLES20.GL_ALPHA;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                if (RGBA8) {
+                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
+                } else {
+                    // Highest precision alpha supported by vanilla OGLES2
+                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
+                }
+                break;
+            case Luminance8:
+                imageFormat.format = GLES20.GL_LUMINANCE;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                if (RGBA8) {
+                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
+                } else {
+                    // Highest precision luminance supported by vanilla OGLES2
+                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
+                }
+                break;
+            case Luminance8Alpha8:
+                imageFormat.format = GLES20.GL_LUMINANCE_ALPHA;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                if (RGBA8) {
+                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
+                } else {
+                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
+                }
+                break;
+            case RGB565:
+                imageFormat.format = GLES20.GL_RGB;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_5_6_5;
+                imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
+                break;
+            case ARGB4444:
+                imageFormat.format = GLES20.GL_RGBA4;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_4_4_4_4;
+                imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
+                break;
+            case RGB5A1:
+                imageFormat.format = GLES20.GL_RGBA;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_5_5_5_1;
+                imageFormat.renderBufferStorageFormat = GLES20.GL_RGB5_A1;
+                break;
+            case RGB8:
+                imageFormat.format = GLES20.GL_RGB;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                if (RGBA8) {
+                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
+                } else {
+                    // Fallback: Use RGB565 if RGBA8 is not available.
+                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
+                }
+                break;
+            case BGR8:
+                imageFormat.format = GLES20.GL_RGB;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                if (RGBA8) {
+                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
+                } else {
+                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
+                }
+                break;
+            case RGBA8:
+                imageFormat.format = GLES20.GL_RGBA;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                if (RGBA8) {
+                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
+                } else {
+                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
+                }
+                break;
+            case Depth:
+            case Depth16:
+                if (!DEPTH_TEXTURE) {
+                    unsupportedFormat(fmt);
+                }
+                imageFormat.format = GLES20.GL_DEPTH_COMPONENT;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT;
+                imageFormat.renderBufferStorageFormat = GLES20.GL_DEPTH_COMPONENT16;
+                break;
+            case Depth24:
+            case Depth24Stencil8:
+                if (!DEPTH_TEXTURE) {
+                    unsupportedFormat(fmt);
+                }
+                if (DEPTH24_STENCIL8) {
+                    // NEW: True Depth24 + Stencil8 format.
+                    imageFormat.format = GL_DEPTH_STENCIL_OES;
+                    imageFormat.dataType = GL_UNSIGNED_INT_24_8_OES;
+                    imageFormat.renderBufferStorageFormat = GL_DEPTH24_STENCIL8_OES;
+                } else {
+                    // Vanilla OGLES2, only Depth16 available.
+                    imageFormat.format = GLES20.GL_DEPTH_COMPONENT;
+                    imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT;
+                    imageFormat.renderBufferStorageFormat = GLES20.GL_DEPTH_COMPONENT16;
+                }
+                break;
+            case DXT1:
+                if (!DXT1) {
+                    unsupportedFormat(fmt);
+                }
+                imageFormat.format = GL_DXT1;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                imageFormat.compress = true;
+                break;
+            case DXT1A:
+                if (!DXT1) {
+                    unsupportedFormat(fmt);
+                }
+                imageFormat.format = GL_DXT1A;
+                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
+                imageFormat.compress = true;
+                break;
+            default:
+                throw new UnsupportedOperationException("Unrecognized format: " + fmt);
+        }
+        return imageFormat;
+    }
+
+    public static class AndroidGLImageFormat {
+
+        boolean compress = false;
+        int format = -1;
+        int renderBufferStorageFormat = -1;
+        int dataType = -1;
+    }
+
+    private static void uploadTexture(Image img,
+            int target,
+            int index) {
+
+        if (img.getEfficentData() instanceof AndroidImageInfo) {
+            throw new RendererException("This image uses efficient data. "
+                    + "Use uploadTextureBitmap instead.");
+        }
+
+        // Otherwise upload image directly.
+        // Prefer to only use power of 2 textures here to avoid errors.
+        Image.Format fmt = img.getFormat();
+        ByteBuffer data;
+        if (index >= 0 || img.getData() != null && img.getData().size() > 0) {
+            data = img.getData(index);
+        } else {
+            data = null;
+        }
+
+        int width = img.getWidth();
+        int height = img.getHeight();
+
+        if (!NPOT) {
+            // Check if texture is POT
+            if (!FastMath.isPowerOfTwo(width) || !FastMath.isPowerOfTwo(height)) {
+                throw new RendererException("Non-power-of-2 textures "
+                        + "are not supported by the video hardware "
+                        + "and no scaling path available for image: " + img);
+            }
+        }
+        AndroidGLImageFormat imageFormat = getImageFormat(fmt);
+
+        if (data != null) {
+            GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
+        }
+
+        int[] mipSizes = img.getMipMapSizes();
+        int pos = 0;
+        if (mipSizes == null) {
+            if (data != null) {
+                mipSizes = new int[]{data.capacity()};
+            } else {
+                mipSizes = new int[]{width * height * fmt.getBitsPerPixel() / 8};
+            }
+        }
+
+        for (int i = 0; i < mipSizes.length; i++) {
+            int mipWidth = Math.max(1, width >> i);
+            int mipHeight = Math.max(1, height >> i);
+
+            if (data != null) {
+                data.position(pos);
+                data.limit(pos + mipSizes[i]);
+            }
+
+            if (imageFormat.compress && data != null) {
+                GLES20.glCompressedTexImage2D(target,
+                        i,
+                        imageFormat.format,
+                        mipWidth,
+                        mipHeight,
+                        0,
+                        data.remaining(),
+                        data);
+            } else {
+                GLES20.glTexImage2D(target,
+                        i,
+                        imageFormat.format,
+                        mipWidth,
+                        mipHeight,
+                        0,
+                        imageFormat.format,
+                        imageFormat.dataType,
+                        data);
+            }
+
+            pos += mipSizes[i];
+        }
+    }
+
+    /**
+     * Update the texture currently bound to target at with data from the given
+     * Image at position x and y. The parameter index is used as the zoffset in
+     * case a 3d texture or texture 2d array is being updated.
+     *
+     * @param image Image with the source data (this data will be put into the
+     * texture)
+     * @param target the target texture
+     * @param index the mipmap level to update
+     * @param x the x position where to put the image in the texture
+     * @param y the y position where to put the image in the texture
+     */
+    public static void uploadSubTexture(
+            Image img,
+            int target,
+            int index,
+            int x,
+            int y) {
+        if (img.getEfficentData() instanceof AndroidImageInfo) {
+            AndroidImageInfo imageInfo = (AndroidImageInfo) img.getEfficentData();
+            uploadTextureBitmap(target, imageInfo.getBitmap(), true, true, x, y);
+            return;
+        }
+
+        // Otherwise upload image directly.
+        // Prefer to only use power of 2 textures here to avoid errors.
+        Image.Format fmt = img.getFormat();
+        ByteBuffer data;
+        if (index >= 0 || img.getData() != null && img.getData().size() > 0) {
+            data = img.getData(index);
+        } else {
+            data = null;
+        }
+
+        int width = img.getWidth();
+        int height = img.getHeight();
+
+        if (!NPOT) {
+            // Check if texture is POT
+            if (!FastMath.isPowerOfTwo(width) || !FastMath.isPowerOfTwo(height)) {
+                throw new RendererException("Non-power-of-2 textures "
+                        + "are not supported by the video hardware "
+                        + "and no scaling path available for image: " + img);
+            }
+        }
+        AndroidGLImageFormat imageFormat = getImageFormat(fmt);
+
+        if (data != null) {
+            GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
+        }
+
+        int[] mipSizes = img.getMipMapSizes();
+        int pos = 0;
+        if (mipSizes == null) {
+            if (data != null) {
+                mipSizes = new int[]{data.capacity()};
+            } else {
+                mipSizes = new int[]{width * height * fmt.getBitsPerPixel() / 8};
+            }
+        }
+
+        for (int i = 0; i < mipSizes.length; i++) {
+            int mipWidth = Math.max(1, width >> i);
+            int mipHeight = Math.max(1, height >> i);
+
+            if (data != null) {
+                data.position(pos);
+                data.limit(pos + mipSizes[i]);
+            }
+
+            if (imageFormat.compress && data != null) {
+                GLES20.glCompressedTexSubImage2D(target, i, x, y, mipWidth, mipHeight, imageFormat.format, data.remaining(), data);
+                RendererUtil.checkGLError();
+            } else {
+                GLES20.glTexSubImage2D(target, i, x, y, mipWidth, mipHeight, imageFormat.format, imageFormat.dataType, data);
+                RendererUtil.checkGLError();
+            }
+
+            pos += mipSizes[i];
+        }
+    }
+}

+ 518 - 0
jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java

@@ -0,0 +1,518 @@
+package com.jme3.system.android;
+
+import android.opengl.GLSurfaceView.EGLConfigChooser;
+import com.jme3.renderer.android.RendererUtil;
+import com.jme3.system.AppSettings;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLDisplay;
+
+/**
+ * AndroidConfigChooser is used to determine the best suited EGL Config
+ *
+ * @author iwgeric
+ */
+public class AndroidConfigChooser implements EGLConfigChooser {
+
+    private static final Logger logger = Logger.getLogger(AndroidConfigChooser.class.getName());
+    protected AppSettings settings;
+    private final static int EGL_OPENGL_ES2_BIT = 4;
+
+
+    @Deprecated
+    public enum ConfigType {
+
+        /**
+         * RGB565, 0 alpha, 16 depth, 0 stencil
+         */
+        FASTEST(5, 6, 5, 0, 16, 0, 5, 6, 5, 0, 16, 0),
+        /**
+         * min RGB888, 0 alpha, 16 depth, 0 stencil max RGB888, 0 alpha, 32
+         * depth, 8 stencil
+         */
+        BEST(8, 8, 8, 0, 32, 8, 8, 8, 8, 0, 16, 0),
+        /**
+         * Turn off config chooser and use hardcoded
+         * setEGLContextClientVersion(2); setEGLConfigChooser(5, 6, 5, 0, 16,
+         * 0);
+         */
+        LEGACY(5, 6, 5, 0, 16, 0, 5, 6, 5, 0, 16, 0),
+        /**
+         * min RGB888, 8 alpha, 16 depth, 0 stencil max RGB888, 8 alpha, 32
+         * depth, 8 stencil
+         */
+        BEST_TRANSLUCENT(8, 8, 8, 8, 32, 8, 8, 8, 8, 8, 16, 0);
+        /**
+         * red, green, blue, alpha, depth, stencil (max values)
+         */
+        int r, g, b, a, d, s;
+        /**
+         * minimal values
+         */
+        int mr, mg, mb, ma, md, ms;
+
+        private ConfigType(int r, int g, int b, int a, int d, int s, int mr, int mg, int mb, int ma, int md, int ms) {
+            this.r = r;
+            this.g = g;
+            this.b = b;
+            this.a = a;
+            this.d = d;
+            this.s = s;
+            this.mr = mr;
+            this.mg = mg;
+            this.mb = mb;
+            this.ma = ma;
+            this.md = md;
+            this.ms = ms;
+        }
+    }
+
+    public AndroidConfigChooser(AppSettings settings) {
+        this.settings = settings;
+    }
+
+    /**
+     * Gets called by the GLSurfaceView class to return the best config
+     */
+    @Override
+    public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+        logger.fine("GLSurfaceView asking for egl config");
+        Config requestedConfig = getRequestedConfig();
+        EGLConfig[] configs = getConfigs(egl, display);
+
+        // First try to find an exact match, but allowing a higher stencil
+        EGLConfig choosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
+        if (choosenConfig == null && requestedConfig.d > 16) {
+            logger.log(Level.INFO, "EGL configuration not found, reducing depth");
+            requestedConfig.d = 16;
+            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
+        }
+
+        if (choosenConfig == null) {
+            logger.log(Level.INFO, "EGL configuration not found, allowing higher RGB");
+            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
+        }
+
+        if (choosenConfig == null && requestedConfig.a > 0) {
+            logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha");
+            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
+        }
+
+        if (choosenConfig == null && requestedConfig.s > 0) {
+            logger.log(Level.INFO, "EGL configuration not found, allowing higher samples");
+            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
+        }
+
+        if (choosenConfig == null && requestedConfig.a > 0) {
+            logger.log(Level.INFO, "EGL configuration not found, reducing alpha");
+            requestedConfig.a = 1;
+            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
+        }
+
+        if (choosenConfig == null && requestedConfig.s > 0) {
+            logger.log(Level.INFO, "EGL configuration not found, reducing samples");
+            requestedConfig.s = 1;
+            if (requestedConfig.a > 0) {
+                choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
+            } else {
+                choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, true, true);
+            }
+        }
+
+        if (choosenConfig == null && requestedConfig.getBitsPerPixel() > 16) {
+            logger.log(Level.INFO, "EGL configuration not found, setting to RGB565");
+            requestedConfig.r = 5;
+            requestedConfig.g = 6;
+            requestedConfig.b = 5;
+            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
+
+            if (choosenConfig == null) {
+                logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha");
+                choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
+            }
+        }
+
+        if (choosenConfig == null) {
+            logger.log(Level.INFO, "EGL configuration not found, looking for best config with >= 16 bit Depth");
+            //failsafe, should pick best config with at least 16 depth
+            requestedConfig = new Config(0, 0, 0, 0, 16, 0, 0);
+            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
+        }
+
+        if (choosenConfig != null) {
+            logger.fine("GLSurfaceView asks for egl config, returning: ");
+            logEGLConfig(choosenConfig, display, egl, Level.FINE);
+
+            storeSelectedConfig(egl, display, choosenConfig);
+            return choosenConfig;
+        } else {
+            logger.severe("No EGL Config found");
+            return null;
+        }
+    }
+
+    private Config getRequestedConfig() {
+        int r, g, b;
+        if (settings.getBitsPerPixel() == 24) {
+            r = g = b = 8;
+        } else {
+            if (settings.getBitsPerPixel() != 16) {
+                logger.log(Level.SEVERE, "Invalid bitsPerPixel setting: {0}, setting to RGB565 (16)", settings.getBitsPerPixel());
+                settings.setBitsPerPixel(16);
+            }
+            r = 5;
+            g = 6;
+            b = 5;
+        }
+        logger.log(Level.FINE, "Requested Display Config:");
+        logger.log(Level.FINE, "RGB: {0}, alpha: {1}, depth: {2}, samples: {3}, stencil: {4}",
+                new Object[]{settings.getBitsPerPixel(),
+                    settings.getAlphaBits(), settings.getDepthBits(),
+                    settings.getSamples(), settings.getStencilBits()});
+        return new Config(
+                r, g, b,
+                settings.getAlphaBits(),
+                settings.getDepthBits(),
+                settings.getSamples(),
+                settings.getStencilBits());
+    }
+
+    /**
+     * Query egl for the available configs
+     * @param egl
+     * @param display
+     * @return
+     */
+    private EGLConfig[] getConfigs(EGL10 egl, EGLDisplay display) {
+
+        int[] num_config = new int[1];
+        int[] configSpec = new int[]{
+            EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+            EGL10.EGL_NONE};
+
+        if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) {
+            RendererUtil.checkEGLError(egl);
+            throw new AssertionError();
+        }
+
+        int numConfigs = num_config[0];
+        EGLConfig[] configs = new EGLConfig[numConfigs];
+        if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, num_config)) {
+            RendererUtil.checkEGLError(egl);
+            throw new AssertionError();
+        }
+
+        logger.fine("--------------Display Configurations---------------");
+        for (EGLConfig eGLConfig : configs) {
+            logEGLConfig(eGLConfig, display, egl, Level.FINE);
+            logger.fine("----------------------------------------");
+        }
+
+        return configs;
+    }
+
+    private EGLConfig chooseConfig(
+            EGL10 egl, EGLDisplay display, EGLConfig[] configs, Config requestedConfig,
+            boolean higherRGB, boolean higherAlpha,
+            boolean higherSamples, boolean higherStencil) {
+
+        EGLConfig keptConfig = null;
+        int kr = 0;
+        int kg = 0;
+        int kb = 0;
+        int ka = 0;
+        int kd = 0;
+        int ks = 0;
+        int kst = 0;
+
+
+        // first pass through config list.  Try to find an exact match.
+        for (EGLConfig config : configs) {
+            int r = eglGetConfigAttribSafe(egl, display, config,
+                    EGL10.EGL_RED_SIZE);
+            int g = eglGetConfigAttribSafe(egl, display, config,
+                    EGL10.EGL_GREEN_SIZE);
+            int b = eglGetConfigAttribSafe(egl, display, config,
+                    EGL10.EGL_BLUE_SIZE);
+            int a = eglGetConfigAttribSafe(egl, display, config,
+                    EGL10.EGL_ALPHA_SIZE);
+            int d = eglGetConfigAttribSafe(egl, display, config,
+                    EGL10.EGL_DEPTH_SIZE);
+            int s = eglGetConfigAttribSafe(egl, display, config,
+                    EGL10.EGL_SAMPLES);
+            int st = eglGetConfigAttribSafe(egl, display, config,
+                    EGL10.EGL_STENCIL_SIZE);
+
+            logger.log(Level.FINE, "Checking Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
+                    new Object[]{r, g, b, a, d, s, st});
+
+            if (higherRGB && r < requestedConfig.r) { continue; }
+            if (!higherRGB && r != requestedConfig.r) { continue; }
+
+            if (higherRGB && g < requestedConfig.g) { continue; }
+            if (!higherRGB && g != requestedConfig.g) { continue; }
+
+            if (higherRGB && b < requestedConfig.b) { continue; }
+            if (!higherRGB && b != requestedConfig.b) { continue; }
+
+            if (higherAlpha && a < requestedConfig.a) { continue; }
+            if (!higherAlpha && a != requestedConfig.a) { continue; }
+
+            if (d < requestedConfig.d) { continue; } // always allow higher depth
+
+            if (higherSamples && s < requestedConfig.s) { continue; }
+            if (!higherSamples && s != requestedConfig.s) { continue; }
+
+            if (higherStencil && st < requestedConfig.st) { continue; }
+            if (!higherStencil && !inRange(st, 0, requestedConfig.st)) { continue; }
+
+            //we keep the config if it is better
+            if (    r >= kr || g >= kg || b >= kb || a >= ka ||
+                    d >= kd || s >= ks || st >= kst ) {
+                kr = r; kg = g; kb = b; ka = a;
+                kd = d; ks = s; kst = st;
+                keptConfig = config;
+                logger.log(Level.FINE, "Keeping Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
+                        new Object[]{r, g, b, a, d, s, st});
+            }
+
+        }
+
+        if (keptConfig != null) {
+            return keptConfig;
+        }
+
+        //no match found
+        logger.log(Level.SEVERE, "No egl config match found");
+        return null;
+    }
+
+    private static int eglGetConfigAttribSafe(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute) {
+        int[] value = new int[1];
+        if (!egl.eglGetConfigAttrib(display, config, attribute, value)) {
+            RendererUtil.checkEGLError(egl);
+            throw new AssertionError();
+        }
+        return value[0];
+    }
+
+    private void storeSelectedConfig(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
+        int r = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_RED_SIZE);
+        int g = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_GREEN_SIZE);
+        int b = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_BLUE_SIZE);
+        settings.setBitsPerPixel(r+g+b);
+
+        settings.setAlphaBits(
+                eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_ALPHA_SIZE));
+        settings.setDepthBits(
+                eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_DEPTH_SIZE));
+        settings.setSamples(
+                eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_SAMPLES));
+        settings.setStencilBits(
+                eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_STENCIL_SIZE));
+    }
+
+    /**
+     * log output with egl config details
+     *
+     * @param conf
+     * @param display
+     * @param egl
+     */
+    private void logEGLConfig(EGLConfig conf, EGLDisplay display, EGL10 egl, Level level) {
+
+        logger.log(level, "EGL_RED_SIZE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RED_SIZE));
+
+        logger.log(level, "EGL_GREEN_SIZE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_GREEN_SIZE));
+
+        logger.log(level, "EGL_BLUE_SIZE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_BLUE_SIZE));
+
+        logger.log(level, "EGL_ALPHA_SIZE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_ALPHA_SIZE));
+
+        logger.log(level, "EGL_DEPTH_SIZE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_DEPTH_SIZE));
+
+        logger.log(level, "EGL_STENCIL_SIZE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_STENCIL_SIZE));
+
+        logger.log(level, "EGL_RENDERABLE_TYPE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RENDERABLE_TYPE));
+
+        logger.log(level, "EGL_SURFACE_TYPE = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SURFACE_TYPE));
+
+        logger.log(level, "EGL_SAMPLE_BUFFERS = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLE_BUFFERS));
+
+        logger.log(level, "EGL_SAMPLES = {0}",
+                eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLES));
+    }
+
+    private boolean inRange(int val, int min, int max) {
+        return min <= val && val <= max;
+    }
+
+    private class Config {
+        /**
+         * red, green, blue, alpha, depth, samples, stencil
+         */
+        int r, g, b, a, d, s, st;
+
+        private Config(int r, int g, int b, int a, int d, int s, int st) {
+            this.r = r;
+            this.g = g;
+            this.b = b;
+            this.a = a;
+            this.d = d;
+            this.s = s;
+            this.st = st;
+        }
+
+        private int getBitsPerPixel() {
+            return r+g+b;
+        }
+    }
+
+//DON'T REMOVE THIS, USED FOR UNIT TESTING FAILING CONFIGURATION LISTS.
+//    private static class Config {
+//
+//        int r, g, b, a, d, s, ms, ns;
+//
+//        public Config(int r, int g, int b, int a, int d, int s, int ms, int ns) {
+//            this.r = r;
+//            this.g = g;
+//            this.b = b;
+//            this.a = a;
+//            this.d = d;
+//            this.s = s;
+//            this.ms = ms;
+//            this.ns = ns;
+//        }
+//
+//        @Override
+//        public String toString() {
+//            return "Config{" + "r=" + r + ", g=" + g + ", b=" + b + ", a=" + a + ", d=" + d + ", s=" + s + ", ms=" + ms + ", ns=" + ns + '}';
+//        }
+//    }
+//
+//    public static Config chooseConfig(List<Config> configs, ConfigType configType, int mSamples) {
+//
+//        Config keptConfig = null;
+//        int kd = 0;
+//        int knbMs = 0;
+//
+//
+//        // first pass through config list.  Try to find an exact match.
+//        for (Config config : configs) {
+////                logEGLConfig(config, display, egl);
+//            int r = config.r;
+//            int g = config.g;
+//            int b = config.b;
+//            int a = config.a;
+//            int d = config.d;
+//            int s = config.s;
+//            int isMs = config.ms;
+//            int nbMs = config.ns;
+//
+//            if (inRange(r, configType.mr, configType.r)
+//                    && inRange(g, configType.mg, configType.g)
+//                    && inRange(b, configType.mb, configType.b)
+//                    && inRange(a, configType.ma, configType.a)
+//                    && inRange(d, configType.md, configType.d)
+//                    && inRange(s, configType.ms, configType.s)) {
+//                if (mSamples == 0 && isMs != 0) {
+//                    continue;
+//                }
+//                boolean keep = false;
+//                //we keep the config if the depth is better or if the AA setting is better
+//                if (d >= kd) {
+//                    kd = d;
+//                    keep = true;
+//                } else {
+//                    keep = false;
+//                }
+//
+//                if (mSamples != 0) {
+//                    if (nbMs >= knbMs && nbMs <= mSamples) {
+//                        knbMs = nbMs;
+//                        keep = true;
+//                    } else {
+//                        keep = false;
+//                    }
+//                }
+//
+//                if (keep) {
+//                    keptConfig = config;
+//                }
+//            }
+//        }
+//
+//        if (keptConfig != null) {
+//            return keptConfig;
+//        }
+//
+//        if (configType == ConfigType.BEST) {
+//            keptConfig = chooseConfig(configs, ConfigType.BEST_TRANSLUCENT, mSamples);
+//
+//            if (keptConfig != null) {
+//                return keptConfig;
+//            }
+//        }
+//
+//        if (configType == ConfigType.BEST_TRANSLUCENT) {
+//            keptConfig = chooseConfig(configs, ConfigType.FASTEST, mSamples);
+//
+//            if (keptConfig != null) {
+//                return keptConfig;
+//            }
+//        }
+//        // failsafe. pick the 1st config.
+//
+//        for (Config config : configs) {
+//            if (config.d >= 16) {
+//                return config;
+//            }
+//        }
+//
+//        return null;
+//    }
+//
+//    private static boolean inRange(int val, int min, int max) {
+//        return min <= val && val <= max;
+//    }
+//
+//    public static void main(String... argv) {
+//        List<Config> confs = new ArrayList<Config>();
+//        confs.add(new Config(5, 6, 5, 0, 0, 0, 0, 0));
+//        confs.add(new Config(5, 6, 5, 0, 16, 0, 0, 0));
+//        confs.add(new Config(5, 6, 5, 0, 24, 8, 0, 0));
+//        confs.add(new Config(8, 8, 8, 8, 0, 0, 0, 0));
+////            confs.add(new Config(8, 8, 8, 8, 16, 0, 0, 0));
+////            confs.add(new Config(8, 8, 8, 8, 24, 8, 0, 0));
+//
+//        confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 2));
+//        confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 2));
+//        confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 2));
+//        confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 2));
+////            confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 2));
+////            confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 2));
+//
+//        confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 4));
+//        confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 4));
+//        confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 4));
+//        confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 4));
+////            confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 4));
+////            confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 4));
+//
+//        Config chosen = chooseConfig(confs, ConfigType.BEST, 0);
+//
+//        System.err.println(chosen);
+//
+//    }
+}

+ 96 - 0
jme3-android/src/main/java/com/jme3/system/android/AndroidTimer.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2009-2012 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.system.android;
+
+import com.jme3.system.Timer;
+
+/**
+ * <code>AndroidTimer</code> is a System.nanoTime implementation of <code>Timer</code>.
+ */
+public class AndroidTimer extends Timer {
+    
+    //private static final long TIMER_RESOLUTION = 1000L;
+    //private static final float INVERSE_TIMER_RESOLUTION = 1f/1000L;
+    private static final long TIMER_RESOLUTION = 1000000000L;
+    private static final float INVERSE_TIMER_RESOLUTION = 1f/1000000000L;
+    
+    private long startTime;
+    private long previousTime;
+    private float tpf;
+    private float fps;
+    
+    public AndroidTimer() {
+        //startTime = System.currentTimeMillis();
+        startTime = System.nanoTime();
+    }
+
+    /**
+     * Returns the time in seconds. The timer starts
+     * at 0.0 seconds.
+     *
+     * @return the current time in seconds
+     */
+    @Override
+    public float getTimeInSeconds() {
+        return getTime() * INVERSE_TIMER_RESOLUTION;
+    }
+
+    public long getTime() {
+        //return System.currentTimeMillis() - startTime;
+        return System.nanoTime() - startTime;
+    }
+
+    public long getResolution() {
+        return TIMER_RESOLUTION;
+    }
+
+    public float getFrameRate() {
+        return fps;
+    }
+
+    public float getTimePerFrame() {
+        return tpf;
+    }
+
+    public void update() {
+        tpf = (getTime() - previousTime) * (1.0f / TIMER_RESOLUTION);
+        fps = 1.0f / tpf;
+        previousTime = getTime();
+    }
+    
+    public void reset() {
+        //startTime = System.currentTimeMillis();
+        startTime = System.nanoTime();
+        previousTime = getTime();
+    }
+}

+ 226 - 0
jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java

@@ -0,0 +1,226 @@
+package com.jme3.system.android;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Environment;
+import com.jme3.asset.AndroidAssetManager;
+import com.jme3.asset.AndroidImageInfo;
+import com.jme3.asset.AssetManager;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.audio.android.AndroidAudioRenderer;
+import com.jme3.audio.android.AndroidMediaPlayerAudioRenderer;
+import com.jme3.audio.android.AndroidOpenALSoftAudioRenderer;
+import com.jme3.system.*;
+import com.jme3.system.JmeContext.Type;
+import com.jme3.texture.Image;
+import com.jme3.texture.image.DefaultImageRaster;
+import com.jme3.texture.image.ImageRaster;
+import com.jme3.util.AndroidScreenshots;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.util.logging.Level;
+
+public class JmeAndroidSystem extends JmeSystemDelegate {
+
+    private static Activity activity;
+    private static String audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
+
+    static {
+        try {
+            System.loadLibrary("bulletjme");
+        } catch (UnsatisfiedLinkError e) {
+        }
+    }
+
+    @Override
+    public void writeImageFile(OutputStream outStream, String format, ByteBuffer imageData, int width, int height) throws IOException {
+        Bitmap bitmapImage = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        AndroidScreenshots.convertScreenShot(imageData, bitmapImage);
+        Bitmap.CompressFormat compressFormat;
+        if (format.equals("png")) {
+            compressFormat = Bitmap.CompressFormat.PNG;
+        } else if (format.equals("jpg")) {
+            compressFormat = Bitmap.CompressFormat.JPEG;
+        } else {
+            throw new UnsupportedOperationException("Only 'png' and 'jpg' formats are supported on Android");
+        }
+        bitmapImage.compress(compressFormat, 95, outStream);
+        bitmapImage.recycle();
+    }
+
+    @Override
+    public ImageRaster createImageRaster(Image image, int slice) {
+        if (image.getEfficentData() != null) {
+            return (AndroidImageInfo) image.getEfficentData();
+        } else {
+            return new DefaultImageRaster(image, slice);
+        }
+    }
+
+    @Override
+    public AssetManager newAssetManager(URL configFile) {
+        logger.log(Level.FINE, "Creating asset manager with config {0}", configFile);
+        return new AndroidAssetManager(configFile);
+    }
+
+    @Override
+    public AssetManager newAssetManager() {
+        logger.log(Level.FINE, "Creating asset manager with default config");
+        return new AndroidAssetManager(null);
+    }
+
+    @Override
+    public void showErrorDialog(String message) {
+        final String finalMsg = message;
+        final String finalTitle = "Error in application";
+        final Activity context = JmeAndroidSystem.getActivity();
+
+        context.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                AlertDialog dialog = new AlertDialog.Builder(context)
+                        .setTitle(finalTitle).setMessage(finalMsg).create();
+                dialog.show();
+            }
+        });
+    }
+
+    @Override
+    public boolean showSettingsDialog(AppSettings sourceSettings, boolean loadFromRegistry) {
+        return true;
+    }
+
+    @Override
+    public JmeContext newContext(AppSettings settings, Type contextType) {
+        if (settings.getAudioRenderer().equals(AppSettings.ANDROID_MEDIAPLAYER)) {
+            audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
+        } else if (settings.getAudioRenderer().equals(AppSettings.ANDROID_OPENAL_SOFT)) {
+            audioRendererType = AppSettings.ANDROID_OPENAL_SOFT;
+        } else {
+            logger.log(Level.INFO, "AudioRenderer not set. Defaulting to Android MediaPlayer / SoundPool");
+            audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
+        }
+        initialize(settings);
+        JmeContext ctx = new OGLESContext();
+        ctx.setSettings(settings);
+        return ctx;
+    }
+
+    @Override
+    public AudioRenderer newAudioRenderer(AppSettings settings) {
+
+        if (settings.getAudioRenderer().equals(AppSettings.ANDROID_MEDIAPLAYER)) {
+            logger.log(Level.INFO, "newAudioRenderer settings set to Android MediaPlayer / SoundPool");
+            audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
+            return new AndroidMediaPlayerAudioRenderer(activity);
+        } else if (settings.getAudioRenderer().equals(AppSettings.ANDROID_OPENAL_SOFT)) {
+            logger.log(Level.INFO, "newAudioRenderer settings set to Android OpenAL Soft");
+            audioRendererType = AppSettings.ANDROID_OPENAL_SOFT;
+            return new AndroidOpenALSoftAudioRenderer();
+        } else {
+            logger.log(Level.INFO, "AudioRenderer not set. Defaulting to Android MediaPlayer / SoundPool");
+            audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
+            return new AndroidMediaPlayerAudioRenderer(activity);
+        }
+    }
+
+    @Override
+    public void initialize(AppSettings settings) {
+        if (initialized) {
+            return;
+        }
+
+        initialized = true;
+
+        logger.log(Level.INFO, "Running on {0}", getFullName());
+    }
+
+    @Override
+    public Platform getPlatform() {
+        String arch = System.getProperty("os.arch").toLowerCase();
+        if (arch.contains("arm")) {
+            if (arch.contains("v5")) {
+                return Platform.Android_ARM5;
+            } else if (arch.contains("v6")) {
+                return Platform.Android_ARM6;
+            } else if (arch.contains("v7")) {
+                return Platform.Android_ARM7;
+            } else {
+                return Platform.Android_ARM5; // unknown ARM
+            }
+        } else {
+            throw new UnsupportedOperationException("Unsupported Android Platform");
+        }
+    }
+
+    @Override
+    public synchronized File getStorageFolder(JmeSystem.StorageFolderType type) {
+        File storageFolder = null;
+
+        switch (type) {
+            case Internal:
+                // http://developer.android.com/guide/topics/data/data-storage.html
+                // http://developer.android.com/guide/topics/data/data-storage.html#filesInternal
+                // http://developer.android.com/reference/android/content/Context.html#getFilesDir()
+                // http://developer.android.com/reference/android/content/Context.html#getDir(java.lang.String, int)
+
+                // getDir automatically creates the directory if necessary.
+                // Directory structure should be: /data/data/<packagename>/app_
+                // When created this way, the directory is automatically removed by the Android
+                //   system when the app is uninstalled.
+                // The directory is NOT accessible by a PC connected to the device
+                // The files can only be accessed by this application
+                storageFolder = storageFolders.get(type);
+                if (storageFolder == null) {
+                    storageFolder = activity.getApplicationContext().getDir("", Context.MODE_PRIVATE);
+                    storageFolders.put(type, storageFolder);
+                }
+                break;
+            case External:
+                //http://developer.android.com/reference/android/content/Context.html#getExternalFilesDir
+                //http://developer.android.com/guide/topics/data/data-storage.html
+
+                // getExternalFilesDir automatically creates the directory if necessary.
+                // Directory structure should be: /mnt/sdcard/Android/data/<packagename>/files
+                // When created this way, the directory is automatically removed by the Android
+                //   system when the app is uninstalled.
+                // The directory is also accessible by a PC connected to the device
+                //   so the files can be copied to the PC (ie. screenshots)
+                storageFolder = storageFolders.get(type);
+                if (storageFolder == null) {
+                    String state = Environment.getExternalStorageState();
+                    logger.log(Level.FINE, "ExternalStorageState: {0}", state);
+                    if (state.equals(Environment.MEDIA_MOUNTED)) {
+                        storageFolder = activity.getApplicationContext().getExternalFilesDir(null);
+                        storageFolders.put(type, storageFolder);
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+        if (storageFolder != null) {
+            logger.log(Level.FINE, "Base Storage Folder Path: {0}", storageFolder.getAbsolutePath());
+        } else {
+            logger.log(Level.FINE, "Base Storage Folder not found!");
+        }
+        return storageFolder;
+    }
+
+    public static void setActivity(Activity activity) {
+        JmeAndroidSystem.activity = activity;
+    }
+
+    public static Activity getActivity() {
+        return activity;
+    }
+
+    public static String getAudioRendererType() {
+        return audioRendererType;
+    }
+}

+ 467 - 0
jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java

@@ -0,0 +1,467 @@
+/*
+ * Copyright (c) 2009-2012 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.system.android;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.ConfigurationInfo;
+import android.graphics.PixelFormat;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.text.InputType;
+import android.view.Gravity;
+import android.view.SurfaceHolder;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import com.jme3.input.*;
+import com.jme3.input.android.AndroidInput;
+import com.jme3.input.android.AndroidSensorJoyInput;
+import com.jme3.input.android.AndroidInputHandler;
+import com.jme3.input.controls.SoftTextDialogInputListener;
+import com.jme3.input.dummy.DummyKeyInput;
+import com.jme3.input.dummy.DummyMouseInput;
+import com.jme3.renderer.android.AndroidGLSurfaceView;
+import com.jme3.renderer.android.OGLESShaderRenderer;
+import com.jme3.system.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTextDialogInput {
+
+    private static final Logger logger = Logger.getLogger(OGLESContext.class.getName());
+    protected final AtomicBoolean created = new AtomicBoolean(false);
+    protected final AtomicBoolean renderable = new AtomicBoolean(false);
+    protected final AtomicBoolean needClose = new AtomicBoolean(false);
+    protected AppSettings settings = new AppSettings(true);
+
+    /*
+     * >= OpenGL ES 2.0 (Android 2.2+)
+     */
+    protected OGLESShaderRenderer renderer;
+    protected Timer timer;
+    protected SystemListener listener;
+    protected boolean autoFlush = true;
+    protected AndroidInputHandler androidInput;
+    protected int minFrameDuration = 0;                   // No FPS cap
+    protected JoyInput androidSensorJoyInput = null;
+    /**
+     * EGL_RENDERABLE_TYPE: EGL_OPENGL_ES_BIT = OpenGL ES 1.0 |
+     * EGL_OPENGL_ES2_BIT = OpenGL ES 2.0
+     */
+    protected int clientOpenGLESVersion = 1;
+
+    public OGLESContext() {
+    }
+
+    @Override
+    public Type getType() {
+        return Type.Display;
+    }
+
+    /**
+     * <code>createView</code> creates the GLSurfaceView that the renderer will
+     * draw to. <p> The result GLSurfaceView will receive input events and
+     * forward them to the Application. Any rendering will be done into the
+     * GLSurfaceView. Only one GLSurfaceView can be created at this time. The
+     * given configType specifies how to determine the display configuration.
+     *
+     * @return GLSurfaceView The newly created view
+     */
+    public AndroidGLSurfaceView createView() {
+        AndroidGLSurfaceView view;
+        int buildVersion = Build.VERSION.SDK_INT;
+
+        // Start to set up the view
+        view = new AndroidGLSurfaceView(JmeAndroidSystem.getActivity().getApplication());
+        if (androidInput == null) {
+            androidInput = new AndroidInputHandler();
+        }
+        androidInput.setView(view);
+        androidInput.loadSettings(settings);
+
+        // setEGLContextClientVersion must be set before calling setRenderer
+        // this means it cannot be set in AndroidConfigChooser (too late)
+        int rawOpenGLESVersion = getOpenGLESVersion();
+//        logger.log(Level.FINE, "clientOpenGLESVersion {0}.{1}",
+//                new Object[]{clientOpenGLESVersion>>16, clientOpenGLESVersion<<16});
+        if (rawOpenGLESVersion < 0x20000) {
+            throw new UnsupportedOperationException("OpenGL ES 2.0 is not supported on this device");
+        } else {
+            clientOpenGLESVersion = 2;
+            view.setEGLContextClientVersion(clientOpenGLESVersion);
+        }
+
+        view.setFocusableInTouchMode(true);
+        view.setFocusable(true);
+        view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_GPU);
+
+        // setFormat must be set before AndroidConfigChooser is called by the surfaceview.
+        // if setFormat is called after ConfigChooser is called, then execution
+        // stops at the setFormat call without a crash.
+        // We look at the user setting for alpha bits and set the surfaceview
+        // PixelFormat to either Opaque, Transparent, or Translucent.
+        // ConfigChooser will do it's best to honor the alpha requested by the user
+        // For best rendering performance, use Opaque (alpha bits = 0).
+        int curAlphaBits = settings.getAlphaBits();
+        logger.log(Level.FINE, "curAlphaBits: {0}", curAlphaBits);
+        if (curAlphaBits >= 8) {
+            logger.log(Level.FINE, "Pixel Format: TRANSLUCENT");
+            view.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+            view.setZOrderOnTop(true);
+        } else if (curAlphaBits >= 1) {
+            logger.log(Level.FINE, "Pixel Format: TRANSPARENT");
+            view.getHolder().setFormat(PixelFormat.TRANSPARENT);
+        } else {
+            logger.log(Level.FINE, "Pixel Format: OPAQUE");
+            view.getHolder().setFormat(PixelFormat.OPAQUE);
+        }
+
+        AndroidConfigChooser configChooser = new AndroidConfigChooser(settings);
+        view.setEGLConfigChooser(configChooser);
+        view.setRenderer(this);
+        
+        // Attempt to preserve the EGL Context on app pause/resume.
+        // Not destroying and recreating the EGL context 
+        // will help with resume time by reusing the existing context to avoid
+        // reloading all the OpenGL objects.
+        if (buildVersion >= 11) {
+            view.setPreserveEGLContextOnPause(true);
+        }
+
+        return view;
+    }
+    /**
+     * Get the  OpenGL ES version
+     * @return version returns the int value of the GLES version
+     */
+    public int getOpenGLESVersion() {
+        ActivityManager am =
+                (ActivityManager) JmeAndroidSystem.getActivity().getApplication().getSystemService(Context.ACTIVITY_SERVICE);
+        ConfigurationInfo info = am.getDeviceConfigurationInfo();
+        logger.log(Level.FINE, "OpenGL Version {0}:", info.getGlEsVersion());
+        return info.reqGlEsVersion;
+//        return (info.reqGlEsVersion >= 0x20000);
+    }
+
+    // renderer:initialize
+    @Override
+    public void onSurfaceCreated(GL10 gl, EGLConfig cfg) {
+        if (created.get() && renderer != null) {
+            renderer.resetGLObjects();
+        } else {
+            if (!created.get()) {
+                logger.fine("GL Surface created, initializing JME3 renderer");
+                initInThread();
+            } else {
+                logger.warning("GL Surface already created");
+            }
+        }
+    }
+
+    protected void initInThread() {
+        created.set(true);
+
+        logger.fine("OGLESContext create");
+        logger.log(Level.FINE, "Running on thread: {0}", Thread.currentThread().getName());
+
+        // Setup unhandled Exception Handler
+        Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+            public void uncaughtException(Thread thread, Throwable thrown) {
+                listener.handleError("Exception thrown in " + thread.toString(), thrown);
+            }
+        });
+
+        timer = new AndroidTimer();
+        renderer = new OGLESShaderRenderer();
+
+        renderer.initialize();
+
+        JmeSystem.setSoftTextDialogInput(this);
+
+        needClose.set(false);
+    }
+
+    /**
+     * De-initialize in the OpenGL thread.
+     */
+    protected void deinitInThread() {
+        if (renderable.get()) {
+            created.set(false);
+            if (renderer != null) {
+                renderer.cleanup();
+            }
+
+            listener.destroy();
+
+            listener = null;
+            renderer = null;
+            timer = null;
+
+            // do android specific cleaning here
+            logger.fine("Display destroyed.");
+
+            renderable.set(false);
+        }
+    }
+
+    @Override
+    public void setSettings(AppSettings settings) {
+        this.settings.copyFrom(settings);
+        if (androidInput != null) {
+            androidInput.loadSettings(settings);
+        }
+
+    }
+
+    @Override
+    public void setSystemListener(SystemListener listener) {
+        this.listener = listener;
+    }
+
+    @Override
+    public AppSettings getSettings() {
+        return settings;
+    }
+
+    @Override
+    public com.jme3.renderer.Renderer getRenderer() {
+        return renderer;
+    }
+
+    @Override
+    public MouseInput getMouseInput() {
+        return new DummyMouseInput();
+    }
+
+    @Override
+    public KeyInput getKeyInput() {
+        return new DummyKeyInput();
+    }
+
+    @Override
+    public JoyInput getJoyInput() {
+        if (androidSensorJoyInput == null) {
+            androidSensorJoyInput = new AndroidSensorJoyInput();
+        }
+        return androidSensorJoyInput;
+    }
+
+    @Override
+    public TouchInput getTouchInput() {
+        return androidInput;
+    }
+
+    @Override
+    public Timer getTimer() {
+        return timer;
+    }
+
+    @Override
+    public void setTitle(String title) {
+    }
+
+    @Override
+    public boolean isCreated() {
+        return created.get();
+    }
+
+    @Override
+    public void setAutoFlushFrames(boolean enabled) {
+        this.autoFlush = enabled;
+    }
+
+    // SystemListener:reshape
+    @Override
+    public void onSurfaceChanged(GL10 gl, int width, int height) {
+        logger.log(Level.FINE, "GL Surface changed, width: {0} height: {1}", new Object[]{width, height});
+        // update the application settings with the new resolution
+        settings.setResolution(width, height);
+        // reload settings in androidInput so the correct touch event scaling can be
+        // calculated in case the surface resolution is different than the view
+        androidInput.loadSettings(settings);
+        // if the application has already been initialized (ie renderable is set)
+        // then call reshape so the app can adjust to the new resolution.
+        if (renderable.get()) {
+            logger.log(Level.FINE, "App already initialized, calling reshape");
+            listener.reshape(width, height);
+        }
+    }
+
+    // SystemListener:update
+    @Override
+    public void onDrawFrame(GL10 gl) {
+        if (needClose.get()) {
+            deinitInThread();
+            return;
+        }
+
+        if (!renderable.get()) {
+            if (created.get()) {
+                logger.fine("GL Surface is setup, initializing application");
+                listener.initialize();
+                renderable.set(true);
+            }
+        } else {
+            if (!created.get()) {
+                throw new IllegalStateException("onDrawFrame without create");
+            }
+
+            long milliStart = System.currentTimeMillis();
+
+            listener.update();
+            if (autoFlush) {
+                renderer.onFrame();
+            }
+
+            long milliDelta = System.currentTimeMillis() - milliStart;
+
+            // Enforce a FPS cap
+            if (milliDelta < minFrameDuration) {
+                //logger.log(Level.FINE, "Time per frame {0}", milliDelta);
+                try {
+                    Thread.sleep(minFrameDuration - milliDelta);
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean isRenderable() {
+        return renderable.get();
+    }
+
+    @Override
+    public void create(boolean waitFor) {
+        if (waitFor) {
+            waitFor(true);
+        }
+    }
+
+    public void create() {
+        create(false);
+    }
+
+    @Override
+    public void restart() {
+    }
+
+    @Override
+    public void destroy(boolean waitFor) {
+        needClose.set(true);
+        if (waitFor) {
+            waitFor(false);
+        }
+    }
+
+    public void destroy() {
+        destroy(true);
+    }
+
+    protected void waitFor(boolean createdVal) {
+        while (renderable.get() != createdVal) {
+            try {
+                Thread.sleep(10);
+            } catch (InterruptedException ex) {
+            }
+        }
+    }
+
+    public void requestDialog(final int id, final String title, final String initialValue, final SoftTextDialogInputListener listener) {
+        logger.log(Level.FINE, "requestDialog: title: {0}, initialValue: {1}",
+                new Object[]{title, initialValue});
+
+        final Activity activity = JmeAndroidSystem.getActivity();
+        activity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                final FrameLayout layoutTextDialogInput = new FrameLayout(activity);
+                final EditText editTextDialogInput = new EditText(activity);
+                editTextDialogInput.setWidth(LayoutParams.FILL_PARENT);
+                editTextDialogInput.setHeight(LayoutParams.FILL_PARENT);
+                editTextDialogInput.setPadding(20, 20, 20, 20);
+                editTextDialogInput.setGravity(Gravity.FILL_HORIZONTAL);
+                //editTextDialogInput.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+
+                editTextDialogInput.setText(initialValue);
+
+                switch (id) {
+                    case SoftTextDialogInput.TEXT_ENTRY_DIALOG:
+
+                        editTextDialogInput.setInputType(InputType.TYPE_CLASS_TEXT);
+                        break;
+
+                    case SoftTextDialogInput.NUMERIC_ENTRY_DIALOG:
+
+                        editTextDialogInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
+                        break;
+
+                    case SoftTextDialogInput.NUMERIC_KEYPAD_DIALOG:
+
+                        editTextDialogInput.setInputType(InputType.TYPE_CLASS_PHONE);
+                        break;
+
+                    default:
+                        break;
+                }
+
+                layoutTextDialogInput.addView(editTextDialogInput);
+
+                AlertDialog dialogTextInput = new AlertDialog.Builder(activity).setTitle(title).setView(layoutTextDialogInput).setPositiveButton("OK",
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int whichButton) {
+                                /* User clicked OK, send COMPLETE action
+                                 * and text */
+                                listener.onSoftText(SoftTextDialogInputListener.COMPLETE, editTextDialogInput.getText().toString());
+                            }
+                        }).setNegativeButton("Cancel",
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int whichButton) {
+                                /* User clicked CANCEL, send CANCEL action
+                                 * and text */
+                                listener.onSoftText(SoftTextDialogInputListener.CANCEL, editTextDialogInput.getText().toString());
+                            }
+                        }).create();
+
+                dialogTextInput.show();
+            }
+        });
+    }
+}

+ 20 - 0
jme3-android/src/main/java/com/jme3/texture/plugins/AndroidImageLoader.java

@@ -0,0 +1,20 @@
+package com.jme3.texture.plugins;
+
+import android.graphics.Bitmap;
+import com.jme3.asset.AndroidImageInfo;
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.texture.Image;
+import java.io.IOException;
+
+public class AndroidImageLoader implements AssetLoader {
+
+    public Object load(AssetInfo info) throws IOException {
+        AndroidImageInfo imageInfo = new AndroidImageInfo(info);
+        Bitmap bitmap = imageInfo.getBitmap();
+        
+        Image image = new Image(imageInfo.getFormat(), bitmap.getWidth(), bitmap.getHeight(), null);
+        image.setEfficentData(imageInfo);
+        return image;
+    }
+}

+ 110 - 0
jme3-android/src/main/java/com/jme3/util/AndroidLogHandler.java

@@ -0,0 +1,110 @@
+package com.jme3.util;
+
+import android.util.Log;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * Converts from Java based logging ({@link Logger} to Android based logging
+ * {@link Log}.
+ */
+public class AndroidLogHandler extends Handler {
+
+    private static final Formatter JME_FORMATTER = new JmeFormatter() {
+
+        String lineSeperator = System.getProperty("line.separator");
+
+        @Override
+        public String format(LogRecord record) {
+            StringBuilder sb = new StringBuilder();
+
+            sb.append(record.getLevel().getLocalizedName()).append(" ");
+            sb.append(formatMessage(record)).append(lineSeperator);
+
+            if (record.getThrown() != null) {
+                try {
+                    StringWriter sw = new StringWriter();
+                    PrintWriter pw = new PrintWriter(sw);
+                    record.getThrown().printStackTrace(pw);
+                    pw.close();
+                    sb.append(sw.toString());
+                } catch (Exception ex) {
+                }
+            }
+
+            return sb.toString();
+        }
+
+    };
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public void flush() {
+    }
+
+    @Override
+    public void publish(LogRecord record) {
+
+        int level = getAndroidLevel(record.getLevel());
+//        String tag = loggerNameToTag(record.getLoggerName());
+        String tag = record.getLoggerName();
+
+        try {
+            String message = JME_FORMATTER.format(record);
+            Log.println(level, tag, message);
+        } catch (RuntimeException e) {
+            Log.e("AndroidHandler", "Error logging message.", e);
+        }
+    }
+
+    /**
+     * Converts a {@link java.util.logging.Logger} logging level into an Android
+     * one.
+     *
+     * @param level The {@link java.util.logging.Logger} logging level.
+     *
+     * @return The resulting Android logging level.
+     */
+    static int getAndroidLevel(Level level) {
+        int value = level.intValue();
+        if (value >= 1000) { // SEVERE
+            return Log.ERROR;
+        } else if (value >= 900) { // WARNING
+            return Log.WARN;
+        } else if (value >= 800) { // INFO
+            return Log.INFO;
+        } else {
+            return Log.DEBUG;
+        }
+    }
+
+    /**
+     * Returns the short logger tag for the given logger name.
+     * Traditionally loggers are named by fully-qualified Java classes; this
+     * method attempts to return a concise identifying part of such names.
+     */
+    public static String loggerNameToTag(String loggerName) {
+        // Anonymous logger.
+        if (loggerName == null) {
+            return "null";
+        }
+
+        int length = loggerName.length();
+        int lastPeriod = loggerName.lastIndexOf(".");
+
+        if (lastPeriod == -1) {
+            return loggerName;
+        }
+
+        return loggerName.substring(lastPeriod + 1);
+    }
+
+}

+ 42 - 0
jme3-android/src/main/java/com/jme3/util/AndroidScreenshots.java

@@ -0,0 +1,42 @@
+package com.jme3.util;
+
+import android.graphics.Bitmap;
+import java.nio.ByteBuffer;
+import java.util.logging.Logger;
+
+public final class AndroidScreenshots {
+
+    private static final Logger logger = Logger.getLogger(AndroidScreenshots.class.getName());
+
+    /**
+     * Convert OpenGL GLES20.GL_RGBA to Bitmap.Config.ARGB_8888 and store result
+     * in a Bitmap
+     *
+     * @param buf ByteBuffer that has the pixel color data from OpenGL
+     * @param bitmapImage Bitmap to be used after converting the data
+     */
+    public static void convertScreenShot(ByteBuffer buf, Bitmap bitmapImage) {
+        int width = bitmapImage.getWidth();
+        int height = bitmapImage.getHeight();
+        int size = width * height;
+
+        // Grab data from ByteBuffer as Int Array to manipulate data and send to image
+        int[] data = new int[size];
+        buf.asIntBuffer().get(data);
+
+        // convert from GLES20.GL_RGBA to Bitmap.Config.ARGB_8888
+        // ** need to swap RED and BLUE **
+        for (int idx = 0; idx < data.length; idx++) {
+            int initial = data[idx];
+            int pb = (initial >> 16) & 0xff;
+            int pr = (initial << 16) & 0x00ff0000;
+            int pix1 = (initial & 0xff00ff00) | pr | pb;
+            data[idx] = pix1;
+        }
+
+        // OpenGL and Bitmap have opposite starting points for Y axis (top vs bottom)
+        // Need to write the data in the image from the bottom to the top
+        // Use size-width to indicate start with last row and increment by -width for each row
+        bitmapImage.setPixels(data, size - width, -width, 0, 0, width, height);
+    }
+}

+ 76 - 0
jme3-android/src/main/java/com/jme3/util/RingBuffer.java

@@ -0,0 +1,76 @@
+package com.jme3.util;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Ring buffer (fixed size queue) implementation using a circular array (array
+ * with wrap-around).
+ */
+// suppress unchecked warnings in Java 1.5.0_6 and later
+@SuppressWarnings("unchecked")
+public class RingBuffer<T> implements Iterable<T> {
+
+    private T[] buffer;          // queue elements
+    private int count = 0;          // number of elements on queue
+    private int indexOut = 0;       // index of first element of queue
+    private int indexIn = 0;       // index of next available slot
+
+    // cast needed since no generic array creation in Java
+    public RingBuffer(int capacity) {
+        buffer = (T[]) new Object[capacity];
+    }
+
+    public boolean isEmpty() {
+        return count == 0;
+    }
+
+    public int size() {
+        return count;
+    }
+
+    public void push(T item) {
+        if (count == buffer.length) {
+            throw new RuntimeException("Ring buffer overflow");
+        }
+        buffer[indexIn] = item;
+        indexIn = (indexIn + 1) % buffer.length;     // wrap-around
+        count++;
+    }
+
+    public T pop() {
+        if (isEmpty()) {
+            throw new RuntimeException("Ring buffer underflow");
+        }
+        T item = buffer[indexOut];
+        buffer[indexOut] = null;                  // to help with garbage collection
+        count--;
+        indexOut = (indexOut + 1) % buffer.length; // wrap-around
+        return item;
+    }
+
+    public Iterator<T> iterator() {
+        return new RingBufferIterator();
+    }
+
+    // an iterator, doesn't implement remove() since it's optional
+    private class RingBufferIterator implements Iterator<T> {
+
+        private int i = 0;
+
+        public boolean hasNext() {
+            return i < count;
+        }
+
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        public T next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            return buffer[i++];
+        }
+    }
+}

+ 29 - 0
jme3-android/src/main/java/jme3test/android/AndroidManifest.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.jme3.androiddemo"
+      android:versionCode="6"
+      android:versionName="1.2.2">
+      
+    <uses-sdk android:targetSdkVersion="8" android:minSdkVersion="8" />      
+    
+    <!-- Tell the system that you need ES 2.0. -->
+    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+    <!-- Tell the system that you need distinct touches (for the zoom gesture). -->
+    <uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="true" />
+
+    <application android:icon="@drawable/icon" android:label="@string/app_name">
+        <activity android:name=".DemoMainActivity"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        
+        <activity android:name=".DemoAndroidHarness"
+                  android:label="@string/app_name">
+        </activity>
+        
+    </application>
+</manifest>

+ 54 - 0
jme3-android/src/main/java/jme3test/android/DemoAndroidHarness.java

@@ -0,0 +1,54 @@
+package jme3test.android;
+
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import com.jme3.app.AndroidHarness;
+import com.jme3.system.android.AndroidConfigChooser.ConfigType;
+
+public class DemoAndroidHarness extends AndroidHarness
+{
+    @Override
+    public void onCreate(Bundle savedInstanceState) 
+    {        
+        // Set the application class to run
+        // First Extract the bundle from intent
+        Bundle bundle = getIntent().getExtras();
+
+        //Next extract the values using the key as
+        appClass = bundle.getString("APPCLASSNAME");                
+        
+        
+        String eglConfig = bundle.getString("EGLCONFIG");
+        if (eglConfig.equals("Best"))
+        {
+            eglConfigType = ConfigType.BEST;
+        }
+        else if (eglConfig.equals("Legacy"))
+        {
+            eglConfigType = ConfigType.LEGACY;
+        }
+        else
+        {
+            eglConfigType = ConfigType.FASTEST;    
+        }
+        
+        
+        if (bundle.getBoolean("VERBOSE"))
+        {
+            eglConfigVerboseLogging = true;
+        }
+        else
+        {
+            eglConfigVerboseLogging = false;
+        }
+        
+        
+        exitDialogTitle = "Close Demo?";
+        exitDialogMessage = "Press Yes";
+                        
+        screenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        
+        super.onCreate(savedInstanceState);                
+    }
+
+}

+ 72 - 0
jme3-android/src/main/java/jme3test/android/DemoLaunchAdapter.java

@@ -0,0 +1,72 @@
+package jme3test.android;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+import java.util.List;
+
+/**
+ * The view adapter which gets a list of LaunchEntries and displaqs them
+ * @author larynx
+ *
+ */
+public class DemoLaunchAdapter extends BaseAdapter implements OnClickListener 
+{
+    
+    private Context context;
+
+    private List<DemoLaunchEntry> listDemos;
+
+    public DemoLaunchAdapter(Context context, List<DemoLaunchEntry> listDemos) {
+        this.context = context;
+        this.listDemos = listDemos;
+    }
+
+    public int getCount() {
+        return listDemos.size();
+    }
+
+    public Object getItem(int position) {
+        return listDemos.get(position);
+    }
+
+    public long getItemId(int position) {
+        return position;
+    }
+
+    public View getView(int position, View convertView, ViewGroup viewGroup) {
+        DemoLaunchEntry entry = listDemos.get(position);
+        if (convertView == null) {
+            LayoutInflater inflater = (LayoutInflater) context
+                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            convertView = inflater.inflate(R.layout.demo_row, null);
+        }
+        TextView tvDemoName = (TextView) convertView.findViewById(R.id.tvDemoName);
+        tvDemoName.setText(entry.getName());
+
+        TextView tvDescription = (TextView) convertView.findViewById(R.id.tvDescription);
+        tvDescription.setText(entry.getDescription());
+        
+        return convertView;
+    }
+
+    @Override
+    public void onClick(View view) {
+        DemoLaunchEntry entry = (DemoLaunchEntry) view.getTag();
+        
+        
+        
+
+    }
+
+    private void showDialog(DemoLaunchEntry entry) {
+        // Create and show your dialog
+        // Depending on the Dialogs button clicks delete it or do nothing
+    }
+
+}
+

+ 38 - 0
jme3-android/src/main/java/jme3test/android/DemoLaunchEntry.java

@@ -0,0 +1,38 @@
+package jme3test.android;
+
+/**
+ * Name (=appClass) and Description of one demo launch inside the main apk
+ * @author larynx
+ *
+ */
+public class DemoLaunchEntry 
+{
+    private String name;
+    private String description;
+            
+    /**
+     * @param name
+     * @param description
+     */
+    public DemoLaunchEntry(String name, String description) {
+        super();
+        this.name = name;
+        this.description = description;
+    }
+    
+    public String getName() {
+        return name;
+    }
+    public void setName(String name) {
+        this.name = name;
+    }
+    public String getDescription() {
+        return description;
+    }
+    public void setDescription(String description) {
+        this.description = description;
+    }
+    
+    
+
+}

+ 131 - 0
jme3-android/src/main/java/jme3test/android/DemoMainActivity.java

@@ -0,0 +1,131 @@
+package jme3test.android;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.*;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DemoMainActivity extends Activity {
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);       
+        
+        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+                      
+        final Intent myIntent = new Intent(DemoMainActivity.this, DemoAndroidHarness.class);
+        
+        //Next create the bundle and initialize it
+        final Bundle bundle = new Bundle();
+
+
+        final Spinner spinnerConfig = (Spinner) findViewById(R.id.spinnerConfig);
+        ArrayAdapter<CharSequence> adapterDropDownConfig = ArrayAdapter.createFromResource(
+                this, R.array.eglconfig_array, android.R.layout.simple_spinner_item);
+        adapterDropDownConfig.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        spinnerConfig.setAdapter(adapterDropDownConfig);
+
+        
+        spinnerConfig.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+            @Override           
+            public void onItemSelected(AdapterView<?> parent,
+                    View view, int pos, long id) {
+                  Toast.makeText(parent.getContext(), "Set EGLConfig " +
+                      parent.getItemAtPosition(pos).toString(), Toast.LENGTH_LONG).show();
+                  //Add the parameters to bundle as
+                  bundle.putString("EGLCONFIG", parent.getItemAtPosition(pos).toString()); 
+            }
+
+            public void onNothingSelected(AdapterView parent) {
+                  // Do nothing.
+            }
+        });
+        
+        
+        final Spinner spinnerLogging = (Spinner) findViewById(R.id.spinnerLogging);
+        ArrayAdapter<CharSequence> adapterDropDownLogging = ArrayAdapter.createFromResource(
+                this, R.array.logging_array, android.R.layout.simple_spinner_item);
+        adapterDropDownLogging.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        spinnerLogging.setAdapter(adapterDropDownLogging);
+
+        
+        spinnerLogging.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+            @Override           
+            public void onItemSelected(AdapterView<?> parent,
+                    View view, int pos, long id) {
+                  Toast.makeText(parent.getContext(), "Set Logging " +
+                      parent.getItemAtPosition(pos).toString(), Toast.LENGTH_LONG).show();
+                                    
+                  //Add the parameters to bundle as
+                  bundle.putBoolean("VERBOSE", parent.getItemAtPosition(pos).toString().equals("Verbose"));
+            }
+
+            public void onNothingSelected(AdapterView parent) {
+                  // Do nothing.
+            }
+        });
+        
+        
+        ListView list = (ListView) findViewById(R.id.ListView01);
+        list.setClickable(true);
+ 
+        final List<DemoLaunchEntry> listDemos = new ArrayList<DemoLaunchEntry>();
+        
+        listDemos.add(new DemoLaunchEntry("jme3test.android.SimpleTexturedTest", "An field of textured boxes rotating"));
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestSkyLoadingLagoon", "Sky box demonstration with jpg"));
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestSkyLoadingPrimitives", "Sky box demonstration with png"));
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestBumpModel", "Shows a bump mapped well with a moving light"));        
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestNormalMapping", "Shows a normal mapped sphere"));
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestUnshadedModel", "Shows an unshaded model of the sphere"));
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestMovingParticle", "Demonstrates particle effects"));        
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestAmbient", "Positional sound - You sit in a dark cave under a waterfall"));
+        
+        //listDemos.add(new DemoLaunchEntry("jme3test.effect.TestParticleEmitter", ""));
+        //listDemos.add(new DemoLaunchEntry("jme3test.effect.TestPointSprite", ""));
+        //listDemos.add(new DemoLaunchEntry("jme3test.light.TestLightRadius", ""));
+        listDemos.add(new DemoLaunchEntry("jme3test.android.TestMotionPath", "Shows cinematics - see a teapot on its journey - model loading needs a long time - just let it load, looks like freezed"));
+        //listDemos.add(new DemoLaunchEntry("com.jme3.androiddemo.TestSimpleWater", "Post processors - not working correctly due to missing framebuffer support, looks interresting :)"));
+        //listDemos.add(new DemoLaunchEntry("jme3test.model.TestHoverTank", ""));
+        //listDemos.add(new DemoLaunchEntry("jme3test.niftygui.TestNiftyGui", ""));
+        //listDemos.add(new DemoLaunchEntry("com.jme3.androiddemo.TestNiftyGui", ""));
+
+        
+        DemoLaunchAdapter adapterList = new DemoLaunchAdapter(this, listDemos);
+
+        list.setOnItemClickListener(new OnItemClickListener() {
+
+            @Override
+            public void onItemClick(AdapterView<?> arg0, View view, int position, long index) {
+                System.out.println("onItemClick");                               
+                showToast(listDemos.get(position).getName());
+                 
+
+                //Add the parameters to bundle as
+                bundle.putString("APPCLASSNAME", listDemos.get(position).getName());
+
+                //Add this bundle to the intent
+                myIntent.putExtras(bundle);
+
+                //Start the JME3 app harness activity                
+                DemoMainActivity.this.startActivity(myIntent);
+
+            }
+        });
+
+        list.setAdapter(adapterList);
+    }
+
+    private void showToast(String message) {
+        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+    }
+}
+

+ 46 - 0
jme3-android/src/main/java/jme3test/android/R.java

@@ -0,0 +1,46 @@
+/* AUTO-GENERATED FILE.  DO NOT MODIFY.
+ *
+ * This class was automatically generated by the
+ * aapt tool from the resource data it found.  It
+ * should not be modified by hand.
+ */
+
+package jme3test.android;
+
+public final class R {
+    public static final class array {
+        public static final int eglconfig_array=0x7f060000;
+        public static final int logging_array=0x7f060001;
+    }
+    public static final class attr {
+    }
+    public static final class drawable {
+        public static final int icon=0x7f020000;
+    }
+    public static final class id {
+        public static final int LinearLayout01=0x7f070000;
+        public static final int LinearLayout02=0x7f070002;
+        public static final int ListView01=0x7f070009;
+        public static final int TextView01=0x7f070003;
+        public static final int spinnerConfig=0x7f070006;
+        public static final int spinnerLogging=0x7f070008;
+        public static final int tvConfig=0x7f070005;
+        public static final int tvDemoName=0x7f070001;
+        public static final int tvDescription=0x7f070004;
+        public static final int tvLogging=0x7f070007;
+    }
+    public static final class layout {
+        public static final int demo_row=0x7f030000;
+        public static final int main=0x7f030001;
+    }
+    public static final class raw {
+        public static final int oddbounce=0x7f040000;
+    }
+    public static final class string {
+        public static final int app_name=0x7f050000;
+        public static final int eglconfig_prompt=0x7f050001;
+        public static final int eglconfig_text=0x7f050002;
+        public static final int logging_prompt=0x7f050003;
+        public static final int logging_text=0x7f050004;
+    }
+}

+ 40 - 0
jme3-android/src/main/java/jme3test/android/SimpleSoundTest.java

@@ -0,0 +1,40 @@
+package jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioNode;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.InputListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.math.Vector3f;
+
+public class SimpleSoundTest extends SimpleApplication implements InputListener {
+
+    private AudioNode gun;
+    private AudioNode nature;
+
+    @Override
+    public void simpleInitApp() {
+        gun = new AudioNode(assetManager, "Sound/Effects/Gun.wav");
+        gun.setPositional(true);
+        gun.setLocalTranslation(new Vector3f(0, 0, 0));
+        gun.setMaxDistance(100);
+        gun.setRefDistance(5);
+
+        nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", true);
+        nature.setVolume(3);
+        nature.setLooping(true);
+        nature.play();
+
+        inputManager.addMapping("click", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addListener(this, "click");
+
+        rootNode.attachChild(gun);
+        rootNode.attachChild(nature);
+    }
+
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (name.equals("click") && isPressed) {
+            gun.playInstance();
+        }
+    }
+}

+ 150 - 0
jme3-android/src/main/java/jme3test/android/SimpleTexturedTest.java

@@ -0,0 +1,150 @@
+
+/*
+ * Android 2.2+ SimpleTextured test.
+ *
+ * created: Mon Nov  8 00:08:22 EST 2010
+ */
+
+package jme3test.android;
+
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.light.PointLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.texture.Texture;
+import com.jme3.util.TangentBinormalGenerator;
+
+
+public class SimpleTexturedTest extends SimpleApplication {
+
+	private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(SimpleTexturedTest.class.getName());
+
+
+	private Node spheresContainer = new Node("spheres-container");
+
+
+	private boolean lightingEnabled = true;
+	private boolean texturedEnabled = true;
+	private boolean spheres = true;
+
+	@Override
+	public void simpleInitApp() {
+	    
+	    //flyCam.setRotationSpeed(0.01f);
+
+
+		Mesh shapeSphere = null;
+		Mesh shapeBox = null;
+
+
+		shapeSphere = new Sphere(16, 16, .5f);
+		shapeBox = new Box(Vector3f.ZERO, 0.3f, 0.3f, 0.3f);
+
+
+	//	ModelConverter.optimize(geom);
+
+		Texture texture = assetManager.loadTexture(new TextureKey("Interface/Logo/Monkey.jpg"));
+		Texture textureMonkey = assetManager.loadTexture(new TextureKey("Interface/Logo/Monkey.jpg"));
+
+		Material material = null;
+		Material materialMonkey = null;
+
+		if (texturedEnabled) {
+			if (lightingEnabled) {
+				material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+				material.setBoolean("VertexLighting", true);
+				material.setFloat("Shininess", 127);
+				material.setBoolean("LowQuality", true);
+				material.setTexture("DiffuseMap", texture);
+				
+				materialMonkey = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+				materialMonkey.setBoolean("VertexLighting", true);
+				materialMonkey.setFloat("Shininess", 127);
+				materialMonkey.setBoolean("LowQuality", true);
+				materialMonkey.setTexture("DiffuseMap", textureMonkey);
+				
+			} else {
+				material = new Material(assetManager, "Common/MatDefs/Misc/SimpleTextured.j3md");
+				material.setTexture("ColorMap", texture);
+				
+				materialMonkey = new Material(assetManager, "Common/MatDefs/Misc/SimpleTextured.j3md");
+				materialMonkey.setTexture("ColorMap", textureMonkey);
+			}
+		} else {
+			material = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
+			material.setColor("Color", ColorRGBA.Red);
+			materialMonkey = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
+			materialMonkey.setColor("Color", ColorRGBA.Red);			
+		}
+
+		TangentBinormalGenerator.generate(shapeSphere);
+		TangentBinormalGenerator.generate(shapeBox);
+
+		int iFlipper = 0;
+		for (int y = -1; y < 2; y++) {
+			for (int x = -1; x < 2; x++){
+				Geometry geomClone = null;
+				
+				//iFlipper++;
+				if (iFlipper % 2 == 0)
+				{
+					geomClone = new Geometry("geometry-" + y + "-" + x, shapeBox);
+				}
+				else
+				{
+					geomClone = new Geometry("geometry-" + y + "-" + x, shapeSphere);
+				}
+				if (iFlipper % 3 == 0)
+				{
+					geomClone.setMaterial(materialMonkey);
+				}
+				else
+				{
+					geomClone.setMaterial(material);
+				}
+				geomClone.setLocalTranslation(x, y, 0);
+                
+//				Transform t = geom.getLocalTransform().clone();
+//				Transform t2 = geomClone.getLocalTransform().clone();
+//				t.combineWithParent(t2);
+//				geomClone.setLocalTransform(t);
+
+				spheresContainer.attachChild(geomClone); 
+			}
+		}
+
+		spheresContainer.setLocalTranslation(new Vector3f(0, 0, -4f));
+		spheresContainer.setLocalScale(2.0f);
+
+		rootNode.attachChild(spheresContainer);
+
+		PointLight pointLight = new PointLight();
+
+		pointLight.setColor(new ColorRGBA(0.7f, 0.7f, 1.0f, 1.0f));
+
+		pointLight.setPosition(new Vector3f(0f, 0f, 0f));
+		pointLight.setRadius(8);
+
+		rootNode.addLight(pointLight);
+	}
+
+	@Override
+	public void simpleUpdate(float tpf) {
+
+		// secondCounter has been removed from SimpleApplication
+                //if (secondCounter == 0)
+		//	logger.fine("Frames per second: " + timer.getFrameRate());
+
+		spheresContainer.rotate(0.2f * tpf, 0.4f * tpf, 0.8f * tpf);
+	}
+
+}
+

+ 97 - 0
jme3-android/src/main/java/jme3test/android/TestAmbient.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2009-2012 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 jme3test.android;
+
+import android.media.SoundPool;
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioNode;
+import com.jme3.math.Vector3f;
+
+public class TestAmbient extends SimpleApplication {
+
+    private AudioNode footsteps, beep;
+    private AudioNode nature, waves;
+    
+    SoundPool soundPool;
+    
+//    private PointAudioSource waves;
+    private float time = 0;
+    private float nextTime = 1;
+
+    public static void main(String[] args){
+        TestAmbient test = new TestAmbient();
+        test.start();
+    }
+    
+
+    @Override
+    public void simpleInitApp()
+    {     
+        /*
+        footsteps  = new AudioNode(audioRenderer, assetManager, "Sound/Effects/Foot steps.ogg", true);
+        
+        footsteps.setPositional(true);
+        footsteps.setLocalTranslation(new Vector3f(4, -1, 30));
+        footsteps.setMaxDistance(5);
+        footsteps.setRefDistance(1);
+        footsteps.setLooping(true);
+
+        beep = new AudioNode(audioRenderer, assetManager, "Sound/Effects/Beep.ogg", true);
+        beep.setVolume(3);
+        beep.setLooping(true);
+        
+        audioRenderer.playSourceInstance(footsteps);
+        audioRenderer.playSource(beep);
+        */
+        
+        waves  = new AudioNode(assetManager, "Sound/Environment/Ocean Waves.ogg", true);
+        waves.setPositional(true);
+
+        nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", true);
+        
+        waves.setLocalTranslation(new Vector3f(4, -1, 30));
+        waves.setMaxDistance(5);
+        waves.setRefDistance(1);
+        
+        nature.setVolume(3);
+        audioRenderer.playSourceInstance(waves);
+        audioRenderer.playSource(nature);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf)
+    {
+
+    }
+
+}

+ 95 - 0
jme3-android/src/main/java/jme3test/android/TestBumpModel.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2009-2012 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 jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.PointLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.ogre.OgreMeshKey;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.util.TangentBinormalGenerator;
+
+public class TestBumpModel extends SimpleApplication {
+
+    float angle;
+    PointLight pl;
+    Spatial lightMdl;
+
+    public static void main(String[] args){
+        TestBumpModel app = new TestBumpModel();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Spatial signpost = (Spatial) assetManager.loadAsset(new OgreMeshKey("Models/Sign Post/Sign Post.mesh.xml"));
+        signpost.setMaterial( (Material) assetManager.loadMaterial("Models/Sign Post/Sign Post.j3m"));
+        TangentBinormalGenerator.generate(signpost);
+        rootNode.attachChild(signpost);
+
+        lightMdl = new Geometry("Light", new Sphere(10, 10, 0.1f));
+        lightMdl.setMaterial( (Material) assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
+        rootNode.attachChild(lightMdl);
+
+        // flourescent main light
+        pl = new PointLight();
+        pl.setColor(new ColorRGBA(0.88f, 0.92f, 0.95f, 1.0f));
+        rootNode.addLight(pl);
+        
+        AmbientLight al = new AmbientLight();
+        al.setColor(new ColorRGBA(0.44f, 0.40f, 0.20f, 1.0f));
+        rootNode.addLight(al);
+        
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(1,-1,-1).normalizeLocal());
+        dl.setColor(new ColorRGBA(0.92f, 0.85f, 0.8f, 1.0f));
+        rootNode.addLight(dl);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){
+        angle += tpf * 0.25f;
+        angle %= FastMath.TWO_PI;
+
+        pl.setPosition(new Vector3f(FastMath.cos(angle) * 6f, 3f, FastMath.sin(angle) * 6f));
+        lightMdl.setLocalTranslation(pl.getPosition());
+    }
+
+}

+ 102 - 0
jme3-android/src/main/java/jme3test/android/TestMovingParticle.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2009-2012 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 jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.AmbientLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+
+/**
+ * Particle that moves in a circle.
+ *
+ * @author Kirill Vainer
+ */
+public class TestMovingParticle extends SimpleApplication {
+    
+    private ParticleEmitter emit;
+    private float angle = 0;
+    
+    public static void main(String[] args) {
+        TestMovingParticle app = new TestMovingParticle();
+        app.start();
+    }
+    
+    @Override
+    public void simpleInitApp() {
+        emit = new ParticleEmitter("Emitter", Type.Triangle, 300);
+        emit.setGravity(0, 0, 0);
+        emit.setVelocityVariation(1);
+        emit.setLowLife(1);
+        emit.setHighLife(1);
+        emit.setInitialVelocity(new Vector3f(0, .5f, 0));
+        emit.setImagesX(15);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
+        emit.setMaterial(mat);
+        
+        rootNode.attachChild(emit);
+        
+        AmbientLight al = new AmbientLight();
+        al.setColor(new ColorRGBA(0.84f, 0.80f, 0.80f, 1.0f));
+        rootNode.addLight(al);
+        
+        
+        
+        inputManager.addListener(new ActionListener() {
+            
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if ("setNum".equals(name) && isPressed) {
+                    emit.setNumParticles(1000);
+                }
+            }
+        }, "setNum");
+        
+        inputManager.addMapping("setNum", new KeyTrigger(KeyInput.KEY_SPACE));
+    }
+    
+    @Override
+    public void simpleUpdate(float tpf) {
+        angle += tpf;
+        angle %= FastMath.TWO_PI;
+        float x = FastMath.cos(angle) * 2;
+        float y = FastMath.sin(angle) * 2;
+        emit.setLocalTranslation(x, 0, y);
+    }
+}

+ 99 - 0
jme3-android/src/main/java/jme3test/android/TestNormalMapping.java

@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2009-2012 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 jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.PointLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.util.TangentBinormalGenerator;
+
+public class TestNormalMapping extends SimpleApplication {
+
+    float angle;
+    PointLight pl;
+    Spatial lightMdl;
+
+    public static void main(String[] args){
+        TestNormalMapping app = new TestNormalMapping();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Sphere sphMesh = new Sphere(32, 32, 1);
+        sphMesh.setTextureMode(Sphere.TextureMode.Projected);
+        sphMesh.updateGeometry(32, 32, 1, false, false);
+        TangentBinormalGenerator.generate(sphMesh);
+
+        Geometry sphere = new Geometry("Rock Ball", sphMesh);
+        Material mat = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m");
+        sphere.setMaterial(mat);
+        rootNode.attachChild(sphere);
+
+        lightMdl = new Geometry("Light", new Sphere(10, 10, 0.1f));
+        lightMdl.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
+        rootNode.attachChild(lightMdl);
+
+        pl = new PointLight();
+        pl.setColor(ColorRGBA.White);
+        pl.setPosition(new Vector3f(0f, 0f, 4f));
+        rootNode.addLight(pl);
+
+        AmbientLight al = new AmbientLight();
+        al.setColor(new ColorRGBA(0.44f, 0.40f, 0.20f, 1.0f));
+        rootNode.addLight(al);
+       
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(1,-1,-1).normalizeLocal());
+        dl.setColor(new ColorRGBA(0.92f, 0.85f, 0.8f, 1.0f));
+        rootNode.addLight(dl);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){
+        angle += tpf * 0.25f;
+        angle %= FastMath.TWO_PI;
+
+        pl.setPosition(new Vector3f(FastMath.cos(angle) * 4f, 0.5f, FastMath.sin(angle) * 4f));
+        lightMdl.setLocalTranslation(pl.getPosition());
+    }
+
+}

+ 70 - 0
jme3-android/src/main/java/jme3test/android/TestSkyLoadingLagoon.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2009-2012 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 jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.scene.Spatial;
+import com.jme3.texture.Texture;
+import com.jme3.util.SkyFactory;
+
+public class TestSkyLoadingLagoon extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestSkyLoadingLagoon app = new TestSkyLoadingLagoon();
+        app.start();
+    }
+
+    public void simpleInitApp() {
+        
+        Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg");
+        Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg");
+        Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg");
+        Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg");
+        Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg");
+        Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg");
+        
+        
+        /*
+        Texture west = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_x.png");
+        Texture east = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_x.png");
+        Texture north = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_z.png");
+        Texture south = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_z.png");
+        Texture up = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_y.png");
+        Texture down = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_y.png");
+        */
+        
+        Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
+        rootNode.attachChild(sky);
+    }
+
+}

+ 68 - 0
jme3-android/src/main/java/jme3test/android/TestSkyLoadingPrimitives.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2009-2012 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 jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.scene.Spatial;
+import com.jme3.texture.Texture;
+import com.jme3.util.SkyFactory;
+
+public class TestSkyLoadingPrimitives extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestSkyLoadingPrimitives app = new TestSkyLoadingPrimitives();
+        app.start();
+    }
+
+    public void simpleInitApp() {
+        /*
+        Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg");
+        Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg");
+        Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg");
+        Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg");
+        Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg");
+        Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg");
+        */
+
+        Texture west = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_x.png");
+        Texture east = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_x.png");
+        Texture north = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_z.png");
+        Texture south = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_z.png");
+        Texture up = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_y.png");
+        Texture down = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_y.png");
+        
+        Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
+        rootNode.attachChild(sky);
+    }
+
+}

+ 44 - 0
jme3-android/src/main/java/jme3test/android/TestUnshadedModel.java

@@ -0,0 +1,44 @@
+package jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.PointLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.util.TangentBinormalGenerator;
+
+public class TestUnshadedModel extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestUnshadedModel app = new TestUnshadedModel();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Sphere sphMesh = new Sphere(32, 32, 1);
+        sphMesh.setTextureMode(Sphere.TextureMode.Projected);
+        sphMesh.updateGeometry(32, 32, 1, false, false);
+        TangentBinormalGenerator.generate(sphMesh);
+
+        Geometry sphere = new Geometry("Rock Ball", sphMesh);
+        Material mat = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m");
+        mat.setColor("Ambient", ColorRGBA.DarkGray);
+        mat.setColor("Diffuse", ColorRGBA.White);
+        mat.setBoolean("UseMaterialColors", true);
+        sphere.setMaterial(mat);
+        rootNode.attachChild(sphere);
+
+        PointLight pl = new PointLight();
+        pl.setColor(ColorRGBA.White);
+        pl.setPosition(new Vector3f(4f, 0f, 0f));
+        rootNode.addLight(pl);
+
+        AmbientLight al = new AmbientLight();
+        al.setColor(ColorRGBA.White);
+        rootNode.addLight(al);
+    }
+}

+ 31 - 0
jme3-android/src/main/resources/res/layout/about.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+>
+
+    <LinearLayout
+        android:id="@+id/buttonsContainer"
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+    >
+        <TextView  
+            android:layout_width="fill_parent" 
+            android:layout_height="wrap_content" 
+            android:text="copyright (c) 2009-2010 JMonkeyEngine"
+		/>
+
+        <TextView  
+            android:layout_width="fill_parent" 
+            android:layout_height="wrap_content" 
+            android:text="http://www.jmonkeyengine.org"
+		/>
+
+    </LinearLayout>
+
+</LinearLayout>

+ 25 - 0
jme3-android/src/main/resources/res/layout/tests.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+>
+    <LinearLayout
+        android:id="@+id/buttonsContainer"
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent">
+<!--
+		<Button
+			android:id="@+id/SimpleTextured"
+			android:layout_width="wrap_content"
+			android:layout_height="fill_parent"
+			android:text="Simple Textured"
+			android:layout_weight="1"
+		/>
+-->
+    </LinearLayout>
+</LinearLayout>

+ 12 - 0
jme3-android/src/main/resources/res/menu/options.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/about_button"
+        android:title="@string/about"
+	/>
+
+    <item
+        android:id="@+id/quit_button"
+        android:title="@string/quit"
+	/>
+</menu>

+ 6 - 0
jme3-android/src/main/resources/res/values/strings.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">JMEAndroidTest</string>
+    <string name="about">About</string>
+    <string name="quit">Quit</string>
+</resources>

+ 909 - 0
jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java

@@ -0,0 +1,909 @@
+/*
+ * Copyright (c) 2009-2012 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.asset;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+
+import com.jme3.bounding.BoundingVolume;
+import com.jme3.collision.Collidable;
+import com.jme3.collision.CollisionResults;
+import com.jme3.collision.UnsupportedCollisionException;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState.FaceCullMode;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.CameraNode;
+import com.jme3.scene.LightNode;
+import com.jme3.scene.Node;
+import com.jme3.scene.SceneGraphVisitor;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.animations.AnimationData;
+import com.jme3.texture.Texture;
+
+/**
+ * Blender key. Contains path of the blender file and its loading properties.
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class BlenderKey extends ModelKey {
+
+    protected static final int         DEFAULT_FPS               = 25;
+    /**
+     * FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time
+     * between the frames.
+     */
+    protected int                      fps                       = DEFAULT_FPS;
+    /**
+     * This variable is a bitwise flag of FeatureToLoad interface values; By default everything is being loaded.
+     */
+    protected int                      featuresToLoad            = FeaturesToLoad.ALL;
+    /** This variable determines if assets that are not linked to the objects should be loaded. */
+    protected boolean                  loadUnlinkedAssets;
+    /** The root path for all the assets. */
+    protected String                   assetRootPath;
+    /** This variable indicate if Y axis is UP axis. If not then Z is up. By default set to true. */
+    protected boolean                  fixUpAxis                 = true;
+    /** Generated textures resolution (PPU - Pixels Per Unit). */
+    protected int                      generatedTexturePPU       = 128;
+    /**
+     * The name of world settings that the importer will use. If not set or specified name does not occur in the file
+     * then the first world settings in the file will be used.
+     */
+    protected String                   usedWorld;
+    /**
+     * User's default material that is set fo objects that have no material definition in blender. The default value is
+     * null. If the value is null the importer will use its own default material (gray color - like in blender).
+     */
+    protected Material                 defaultMaterial;
+    /** Face cull mode. By default it is disabled. */
+    protected FaceCullMode             faceCullMode              = FaceCullMode.Back;
+    /**
+     * Variable describes which layers will be loaded. N-th bit set means N-th layer will be loaded.
+     * If set to -1 then the current layer will be loaded.
+     */
+    protected int                      layersToLoad              = -1;
+    /** A variable that toggles the object custom properties loading. */
+    protected boolean                  loadObjectProperties      = true;
+    /**
+     * Maximum texture size. Might be dependant on the graphic card.
+     * This value is taken from <b>org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE</b>.
+     */
+    protected int                      maxTextureSize            = 8192;
+    /** Allows to toggle generated textures loading. Disabled by default because it very often takes too much memory and needs to be used wisely. */
+    protected boolean                  loadGeneratedTextures;
+    /** Tells if the mipmaps will be generated by jme or not. By default generation is dependant on the blender settings. */
+    protected MipmapGenerationMethod   mipmapGenerationMethod    = MipmapGenerationMethod.GENERATE_WHEN_NEEDED;
+    /**
+     * If the sky has only generated textures applied then they will have the following size (both width and height). If 2d textures are used then the generated
+     * textures will get their proper size.
+     */
+    protected int                      skyGeneratedTextureSize   = 1000;
+    /** The radius of a shape that will be used while creating the generated texture for the sky. The higher it is the larger part of the texture will be seen. */
+    protected float                    skyGeneratedTextureRadius = 1;
+    /** The shape against which the generated texture for the sky will be created. */
+    protected SkyGeneratedTextureShape skyGeneratedTextureShape  = SkyGeneratedTextureShape.SPHERE;
+    /**
+     * This field tells if the importer should optimise the use of textures or not. If set to true, then textures of the same mapping type will be merged together
+     * and textures that in the final result will never be visible - will be discarded.
+     */
+    protected boolean                  optimiseTextures;
+
+    /**
+     * Constructor used by serialization mechanisms.
+     */
+    public BlenderKey() {
+    }
+
+    /**
+     * Constructor. Creates a key for the given file name.
+     * @param name
+     *            the name (path) of a file
+     */
+    public BlenderKey(String name) {
+        super(name);
+    }
+
+    /**
+     * This method returns frames per second amount. The default value is BlenderKey.DEFAULT_FPS = 25.
+     * @return the frames per second amount
+     */
+    public int getFps() {
+        return fps;
+    }
+
+    /**
+     * This method sets frames per second amount.
+     * @param fps
+     *            the frames per second amount
+     */
+    public void setFps(int fps) {
+        this.fps = fps;
+    }
+
+    /**
+     * This method returns the face cull mode.
+     * @return the face cull mode
+     */
+    public FaceCullMode getFaceCullMode() {
+        return faceCullMode;
+    }
+
+    /**
+     * This method sets the face cull mode.
+     * @param faceCullMode
+     *            the face cull mode
+     */
+    public void setFaceCullMode(FaceCullMode faceCullMode) {
+        this.faceCullMode = faceCullMode;
+    }
+
+    /**
+     * This method sets layers to be loaded.
+     * @param layersToLoad
+     *            layers to be loaded
+     */
+    public void setLayersToLoad(int layersToLoad) {
+        this.layersToLoad = layersToLoad;
+    }
+
+    /**
+     * This method returns layers to be loaded.
+     * @return layers to be loaded
+     */
+    public int getLayersToLoad() {
+        return layersToLoad;
+    }
+
+    /**
+     * This method sets the properies loading policy.
+     * By default the value is true.
+     * @param loadObjectProperties
+     *            true to load properties and false to suspend their loading
+     */
+    public void setLoadObjectProperties(boolean loadObjectProperties) {
+        this.loadObjectProperties = loadObjectProperties;
+    }
+
+    /**
+     * @return the current properties loading properties
+     */
+    public boolean isLoadObjectProperties() {
+        return loadObjectProperties;
+    }
+
+    /**
+     * The default value for this parameter is the same as defined by: org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE.
+     * If by any means this is too large for user's hardware configuration use the 'setMaxTextureSize' method to change that.
+     * @return maximum texture size (width/height)
+     */
+    public int getMaxTextureSize() {
+        return maxTextureSize;
+    }
+
+    /**
+     * This method sets the maximum texture size.
+     * @param maxTextureSize
+     *            the maximum texture size
+     */
+    public void setMaxTextureSize(int maxTextureSize) {
+        this.maxTextureSize = maxTextureSize;
+    }
+
+    /**
+     * This method sets the flag that toggles the generated textures loading.
+     * @param loadGeneratedTextures
+     *            <b>true</b> if generated textures should be loaded and <b>false</b> otherwise
+     */
+    public void setLoadGeneratedTextures(boolean loadGeneratedTextures) {
+        this.loadGeneratedTextures = loadGeneratedTextures;
+    }
+
+    /**
+     * @return tells if the generated textures should be loaded (<b>false</b> is the default value)
+     */
+    public boolean isLoadGeneratedTextures() {
+        return loadGeneratedTextures;
+    }
+
+    /**
+     * This method sets the asset root path.
+     * @param assetRootPath
+     *            the assets root path
+     */
+    public void setAssetRootPath(String assetRootPath) {
+        this.assetRootPath = assetRootPath;
+    }
+
+    /**
+     * This method returns the asset root path.
+     * @return the asset root path
+     */
+    public String getAssetRootPath() {
+        return assetRootPath;
+    }
+
+    /**
+     * This method adds features to be loaded.
+     * @param featuresToLoad
+     *            bitwise flag of FeaturesToLoad interface values
+     */
+    public void includeInLoading(int featuresToLoad) {
+        this.featuresToLoad |= featuresToLoad;
+    }
+
+    /**
+     * This method removes features from being loaded.
+     * @param featuresNotToLoad
+     *            bitwise flag of FeaturesToLoad interface values
+     */
+    public void excludeFromLoading(int featuresNotToLoad) {
+        featuresToLoad &= ~featuresNotToLoad;
+    }
+
+    public boolean shouldLoad(int featureToLoad) {
+        return (featuresToLoad & featureToLoad) != 0;
+    }
+
+    /**
+     * This method returns bitwise value of FeaturesToLoad interface value. It describes features that will be loaded by
+     * the blender file loader.
+     * @return features that will be loaded by the blender file loader
+     */
+    public int getFeaturesToLoad() {
+        return featuresToLoad;
+    }
+
+    /**
+     * This method determines if unlinked assets should be loaded.
+     * If not then only objects on selected layers will be loaded and their assets if required.
+     * If yes then all assets will be loaded even if they are on inactive layers or are not linked
+     * to anything.
+     * @return <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise
+     */
+    public boolean isLoadUnlinkedAssets() {
+        return loadUnlinkedAssets;
+    }
+
+    /**
+     * This method sets if unlinked assets should be loaded.
+     * If not then only objects on selected layers will be loaded and their assets if required.
+     * If yes then all assets will be loaded even if they are on inactive layers or are not linked
+     * to anything.
+     * @param loadUnlinkedAssets
+     *            <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise
+     */
+    public void setLoadUnlinkedAssets(boolean loadUnlinkedAssets) {
+        this.loadUnlinkedAssets = loadUnlinkedAssets;
+    }
+
+    /**
+     * This method creates an object where loading results will be stores. Only those features will be allowed to store
+     * that were specified by features-to-load flag.
+     * @return an object to store loading results
+     */
+    public LoadingResults prepareLoadingResults() {
+        return new LoadingResults(featuresToLoad);
+    }
+
+    /**
+     * This method sets the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By default Y
+     * is up axis.
+     * @param fixUpAxis
+     *            the up axis state variable
+     */
+    public void setFixUpAxis(boolean fixUpAxis) {
+        this.fixUpAxis = fixUpAxis;
+    }
+
+    /**
+     * This method returns the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By
+     * default Y is up axis.
+     * @return the up axis state variable
+     */
+    public boolean isFixUpAxis() {
+        return fixUpAxis;
+    }
+
+    /**
+     * This method sets the generated textures resolution.
+     * @param generatedTexturePPU
+     *            the generated textures resolution
+     */
+    public void setGeneratedTexturePPU(int generatedTexturePPU) {
+        this.generatedTexturePPU = generatedTexturePPU;
+    }
+
+    /**
+     * @return the generated textures resolution
+     */
+    public int getGeneratedTexturePPU() {
+        return generatedTexturePPU;
+    }
+
+    /**
+     * @return mipmaps generation method
+     */
+    public MipmapGenerationMethod getMipmapGenerationMethod() {
+        return mipmapGenerationMethod;
+    }
+
+    /**
+     * @param mipmapGenerationMethod
+     *            mipmaps generation method
+     */
+    public void setMipmapGenerationMethod(MipmapGenerationMethod mipmapGenerationMethod) {
+        this.mipmapGenerationMethod = mipmapGenerationMethod;
+    }
+
+    /**
+     * @return the size of the generated textures for the sky (used if no flat textures are applied)
+     */
+    public int getSkyGeneratedTextureSize() {
+        return skyGeneratedTextureSize;
+    }
+
+    /**
+     * @param skyGeneratedTextureSize
+     *            the size of the generated textures for the sky (used if no flat textures are applied)
+     */
+    public void setSkyGeneratedTextureSize(int skyGeneratedTextureSize) {
+        if (skyGeneratedTextureSize <= 0) {
+            throw new IllegalArgumentException("The texture size must be a positive value (the value given as a parameter: " + skyGeneratedTextureSize + ")!");
+        }
+        this.skyGeneratedTextureSize = skyGeneratedTextureSize;
+    }
+
+    /**
+     * @return the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen
+     */
+    public float getSkyGeneratedTextureRadius() {
+        return skyGeneratedTextureRadius;
+    }
+
+    /**
+     * @param skyGeneratedTextureRadius
+     *            the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen
+     */
+    public void setSkyGeneratedTextureRadius(float skyGeneratedTextureRadius) {
+        this.skyGeneratedTextureRadius = skyGeneratedTextureRadius;
+    }
+
+    /**
+     * @return the shape against which the generated texture for the sky will be created (by default it is a sphere).
+     */
+    public SkyGeneratedTextureShape getSkyGeneratedTextureShape() {
+        return skyGeneratedTextureShape;
+    }
+
+    /**
+     * @param skyGeneratedTextureShape
+     *            the shape against which the generated texture for the sky will be created
+     */
+    public void setSkyGeneratedTextureShape(SkyGeneratedTextureShape skyGeneratedTextureShape) {
+        if (skyGeneratedTextureShape == null) {
+            throw new IllegalArgumentException("The sky generated shape type cannot be null!");
+        }
+        this.skyGeneratedTextureShape = skyGeneratedTextureShape;
+    }
+
+    /**
+     * If set to true, then textures of the same mapping type will be merged together
+     * and textures that in the final result will never be visible - will be discarded.
+     * @param optimiseTextures
+     *            the variable that tells if the textures should be optimised or not
+     */
+    public void setOptimiseTextures(boolean optimiseTextures) {
+        this.optimiseTextures = optimiseTextures;
+    }
+
+    /**
+     * @return the variable that tells if the textures should be optimised or not (by default the optimisation is disabled)
+     */
+    public boolean isOptimiseTextures() {
+        return optimiseTextures;
+    }
+
+    /**
+     * This mehtod sets the name of the WORLD data block taht should be used during file loading. By default the name is
+     * not set. If no name is set or the given name does not occur in the file - the first WORLD data block will be used
+     * during loading (assumin any exists in the file).
+     * @param usedWorld
+     *            the name of the WORLD block used during loading
+     */
+    public void setUsedWorld(String usedWorld) {
+        this.usedWorld = usedWorld;
+    }
+
+    /**
+     * This mehtod returns the name of the WORLD data block taht should be used during file loading.
+     * @return the name of the WORLD block used during loading
+     */
+    public String getUsedWorld() {
+        return usedWorld;
+    }
+
+    /**
+     * This method sets the default material for objects.
+     * @param defaultMaterial
+     *            the default material
+     */
+    public void setDefaultMaterial(Material defaultMaterial) {
+        this.defaultMaterial = defaultMaterial;
+    }
+
+    /**
+     * This method returns the default material.
+     * @return the default material
+     */
+    public Material getDefaultMaterial() {
+        return defaultMaterial;
+    }
+
+    @Override
+    public void write(JmeExporter e) throws IOException {
+        super.write(e);
+        OutputCapsule oc = e.getCapsule(this);
+        oc.write(fps, "fps", DEFAULT_FPS);
+        oc.write(featuresToLoad, "features-to-load", FeaturesToLoad.ALL);
+        oc.write(loadUnlinkedAssets, "load-unlinked-assets", false);
+        oc.write(assetRootPath, "asset-root-path", null);
+        oc.write(fixUpAxis, "fix-up-axis", true);
+        oc.write(generatedTexturePPU, "generated-texture-ppu", 128);
+        oc.write(usedWorld, "used-world", null);
+        oc.write(defaultMaterial, "default-material", null);
+        oc.write(faceCullMode, "face-cull-mode", FaceCullMode.Off);
+        oc.write(layersToLoad, "layers-to-load", -1);
+        oc.write(mipmapGenerationMethod, "mipmap-generation-method", MipmapGenerationMethod.GENERATE_WHEN_NEEDED);
+        oc.write(skyGeneratedTextureSize, "sky-generated-texture-size", 1000);
+        oc.write(skyGeneratedTextureRadius, "sky-generated-texture-radius", 1f);
+        oc.write(skyGeneratedTextureShape, "sky-generated-texture-shape", SkyGeneratedTextureShape.SPHERE);
+        oc.write(optimiseTextures, "optimise-textures", false);
+    }
+
+    @Override
+    public void read(JmeImporter e) throws IOException {
+        super.read(e);
+        InputCapsule ic = e.getCapsule(this);
+        fps = ic.readInt("fps", DEFAULT_FPS);
+        featuresToLoad = ic.readInt("features-to-load", FeaturesToLoad.ALL);
+        loadUnlinkedAssets = ic.readBoolean("load-unlinked-assets", false);
+        assetRootPath = ic.readString("asset-root-path", null);
+        fixUpAxis = ic.readBoolean("fix-up-axis", true);
+        generatedTexturePPU = ic.readInt("generated-texture-ppu", 128);
+        usedWorld = ic.readString("used-world", null);
+        defaultMaterial = (Material) ic.readSavable("default-material", null);
+        faceCullMode = ic.readEnum("face-cull-mode", FaceCullMode.class, FaceCullMode.Off);
+        layersToLoad = ic.readInt("layers-to=load", -1);
+        mipmapGenerationMethod = ic.readEnum("mipmap-generation-method", MipmapGenerationMethod.class, MipmapGenerationMethod.GENERATE_WHEN_NEEDED);
+        skyGeneratedTextureSize = ic.readInt("sky-generated-texture-size", 1000);
+        skyGeneratedTextureRadius = ic.readFloat("sky-generated-texture-radius", 1f);
+        skyGeneratedTextureShape = ic.readEnum("sky-generated-texture-shape", SkyGeneratedTextureShape.class, SkyGeneratedTextureShape.SPHERE);
+        optimiseTextures = ic.readBoolean("optimise-textures", false);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + (assetRootPath == null ? 0 : assetRootPath.hashCode());
+        result = prime * result + (defaultMaterial == null ? 0 : defaultMaterial.hashCode());
+        result = prime * result + (faceCullMode == null ? 0 : faceCullMode.hashCode());
+        result = prime * result + featuresToLoad;
+        result = prime * result + (fixUpAxis ? 1231 : 1237);
+        result = prime * result + fps;
+        result = prime * result + generatedTexturePPU;
+        result = prime * result + layersToLoad;
+        result = prime * result + (loadGeneratedTextures ? 1231 : 1237);
+        result = prime * result + (loadObjectProperties ? 1231 : 1237);
+        result = prime * result + (loadUnlinkedAssets ? 1231 : 1237);
+        result = prime * result + maxTextureSize;
+        result = prime * result + (mipmapGenerationMethod == null ? 0 : mipmapGenerationMethod.hashCode());
+        result = prime * result + (optimiseTextures ? 1231 : 1237);
+        result = prime * result + Float.floatToIntBits(skyGeneratedTextureRadius);
+        result = prime * result + (skyGeneratedTextureShape == null ? 0 : skyGeneratedTextureShape.hashCode());
+        result = prime * result + skyGeneratedTextureSize;
+        result = prime * result + (usedWorld == null ? 0 : usedWorld.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (this.getClass() != obj.getClass()) {
+            return false;
+        }
+        BlenderKey other = (BlenderKey) obj;
+        if (assetRootPath == null) {
+            if (other.assetRootPath != null) {
+                return false;
+            }
+        } else if (!assetRootPath.equals(other.assetRootPath)) {
+            return false;
+        }
+        if (defaultMaterial == null) {
+            if (other.defaultMaterial != null) {
+                return false;
+            }
+        } else if (!defaultMaterial.equals(other.defaultMaterial)) {
+            return false;
+        }
+        if (faceCullMode != other.faceCullMode) {
+            return false;
+        }
+        if (featuresToLoad != other.featuresToLoad) {
+            return false;
+        }
+        if (fixUpAxis != other.fixUpAxis) {
+            return false;
+        }
+        if (fps != other.fps) {
+            return false;
+        }
+        if (generatedTexturePPU != other.generatedTexturePPU) {
+            return false;
+        }
+        if (layersToLoad != other.layersToLoad) {
+            return false;
+        }
+        if (loadGeneratedTextures != other.loadGeneratedTextures) {
+            return false;
+        }
+        if (loadObjectProperties != other.loadObjectProperties) {
+            return false;
+        }
+        if (loadUnlinkedAssets != other.loadUnlinkedAssets) {
+            return false;
+        }
+        if (maxTextureSize != other.maxTextureSize) {
+            return false;
+        }
+        if (mipmapGenerationMethod != other.mipmapGenerationMethod) {
+            return false;
+        }
+        if (optimiseTextures != other.optimiseTextures) {
+            return false;
+        }
+        if (Float.floatToIntBits(skyGeneratedTextureRadius) != Float.floatToIntBits(other.skyGeneratedTextureRadius)) {
+            return false;
+        }
+        if (skyGeneratedTextureShape != other.skyGeneratedTextureShape) {
+            return false;
+        }
+        if (skyGeneratedTextureSize != other.skyGeneratedTextureSize) {
+            return false;
+        }
+        if (usedWorld == null) {
+            if (other.usedWorld != null) {
+                return false;
+            }
+        } else if (!usedWorld.equals(other.usedWorld)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * This enum tells the importer if the mipmaps for textures will be generated by jme. <li>NEVER_GENERATE and ALWAYS_GENERATE are quite understandable <li>GENERATE_WHEN_NEEDED is an option that checks if the texture had 'Generate mipmaps' option set in blender, mipmaps are generated only when the option is set
+     * @author Marcin Roguski (Kaelthas)
+     */
+    public static enum MipmapGenerationMethod {
+        NEVER_GENERATE, ALWAYS_GENERATE, GENERATE_WHEN_NEEDED;
+    }
+
+    /**
+     * This interface describes the features of the scene that are to be loaded.
+     * @author Marcin Roguski (Kaelthas)
+     */
+    public static interface FeaturesToLoad {
+
+        int SCENES     = 0x0000FFFF;
+        int OBJECTS    = 0x0000000B;
+        int ANIMATIONS = 0x00000004;
+        int MATERIALS  = 0x00000003;
+        int TEXTURES   = 0x00000001;
+        int CAMERAS    = 0x00000020;
+        int LIGHTS     = 0x00000010;
+        int WORLD      = 0x00000040;
+        int ALL        = 0xFFFFFFFF;
+    }
+
+    /**
+     * The shape againts which the sky generated texture will be created.
+     * 
+     * @author Marcin Roguski (Kaelthas)
+     */
+    public static enum SkyGeneratedTextureShape {
+        CUBE, SPHERE;
+    }
+
+    /**
+     * This class holds the loading results according to the given loading flag.
+     * @author Marcin Roguski (Kaelthas)
+     */
+    public static class LoadingResults extends Spatial {
+
+        /** Bitwise mask of features that are to be loaded. */
+        private final int           featuresToLoad;
+        /** The scenes from the file. */
+        private List<Node>          scenes;
+        /** Objects from all scenes. */
+        private List<Node>          objects;
+        /** Materials from all objects. */
+        private List<Material>      materials;
+        /** Textures from all objects. */
+        private List<Texture>       textures;
+        /** Animations of all objects. */
+        private List<AnimationData> animations;
+        /** All cameras from the file. */
+        private List<CameraNode>    cameras;
+        /** All lights from the file. */
+        private List<LightNode>     lights;
+        /** Loaded sky. */
+        private Spatial             sky;
+        /**
+         * The background color of the render loaded from the horizon color of the world. If no world is used than the gray color
+         * is set to default (as in blender editor.
+         */
+        private ColorRGBA           backgroundColor = ColorRGBA.Gray;
+
+        /**
+         * Private constructor prevents users to create an instance of this class from outside the
+         * @param featuresToLoad
+         *            bitwise mask of features that are to be loaded
+         * @see FeaturesToLoad FeaturesToLoad
+         */
+        private LoadingResults(int featuresToLoad) {
+            this.featuresToLoad = featuresToLoad;
+            if ((featuresToLoad & FeaturesToLoad.SCENES) != 0) {
+                scenes = new ArrayList<Node>();
+            }
+            if ((featuresToLoad & FeaturesToLoad.OBJECTS) != 0) {
+                objects = new ArrayList<Node>();
+                if ((featuresToLoad & FeaturesToLoad.MATERIALS) != 0) {
+                    materials = new ArrayList<Material>();
+                    if ((featuresToLoad & FeaturesToLoad.TEXTURES) != 0) {
+                        textures = new ArrayList<Texture>();
+                    }
+                }
+                if ((featuresToLoad & FeaturesToLoad.ANIMATIONS) != 0) {
+                    animations = new ArrayList<AnimationData>();
+                }
+            }
+            if ((featuresToLoad & FeaturesToLoad.CAMERAS) != 0) {
+                cameras = new ArrayList<CameraNode>();
+            }
+            if ((featuresToLoad & FeaturesToLoad.LIGHTS) != 0) {
+                lights = new ArrayList<LightNode>();
+            }
+        }
+
+        /**
+         * This method returns a bitwise flag describing what features of the blend file will be included in the result.
+         * @return bitwise mask of features that are to be loaded
+         * @see FeaturesToLoad FeaturesToLoad
+         */
+        public int getLoadedFeatures() {
+            return featuresToLoad;
+        }
+
+        /**
+         * This method adds a scene to the result set.
+         * @param scene
+         *            scene to be added to the result set
+         */
+        public void addScene(Node scene) {
+            if (scenes != null) {
+                scenes.add(scene);
+            }
+        }
+
+        /**
+         * This method adds an object to the result set.
+         * @param object
+         *            object to be added to the result set
+         */
+        public void addObject(Node object) {
+            if (objects != null) {
+                objects.add(object);
+            }
+        }
+
+        /**
+         * This method adds a material to the result set.
+         * @param material
+         *            material to be added to the result set
+         */
+        public void addMaterial(Material material) {
+            if (materials != null) {
+                materials.add(material);
+            }
+        }
+
+        /**
+         * This method adds a texture to the result set.
+         * @param texture
+         *            texture to be added to the result set
+         */
+        public void addTexture(Texture texture) {
+            if (textures != null) {
+                textures.add(texture);
+            }
+        }
+
+        /**
+         * This method adds a camera to the result set.
+         * @param camera
+         *            camera to be added to the result set
+         */
+        public void addCamera(CameraNode camera) {
+            if (cameras != null) {
+                cameras.add(camera);
+            }
+        }
+
+        /**
+         * This method adds a light to the result set.
+         * @param light
+         *            light to be added to the result set
+         */
+        public void addLight(LightNode light) {
+            if (lights != null) {
+                lights.add(light);
+            }
+        }
+
+        /**
+         * This method sets the sky of the scene. Only one sky can be set.
+         * @param sky
+         *            the sky to be set
+         */
+        public void setSky(Spatial sky) {
+            this.sky = sky;
+        }
+
+        /**
+         * @param backgroundColor
+         *            the background color
+         */
+        public void setBackgroundColor(ColorRGBA backgroundColor) {
+            this.backgroundColor = backgroundColor;
+        }
+
+        /**
+         * @return all loaded scenes
+         */
+        public List<Node> getScenes() {
+            return scenes;
+        }
+
+        /**
+         * @return all loaded objects
+         */
+        public List<Node> getObjects() {
+            return objects;
+        }
+
+        /**
+         * @return all loaded materials
+         */
+        public List<Material> getMaterials() {
+            return materials;
+        }
+
+        /**
+         * @return all loaded textures
+         */
+        public List<Texture> getTextures() {
+            return textures;
+        }
+
+        /**
+         * @return all loaded animations
+         */
+        public List<AnimationData> getAnimations() {
+            return animations;
+        }
+
+        /**
+         * @return all loaded cameras
+         */
+        public List<CameraNode> getCameras() {
+            return cameras;
+        }
+
+        /**
+         * @return all loaded lights
+         */
+        public List<LightNode> getLights() {
+            return lights;
+        }
+
+        /**
+         * @return the scene's sky
+         */
+        public Spatial getSky() {
+            return sky;
+        }
+
+        /**
+         * @return the background color
+         */
+        public ColorRGBA getBackgroundColor() {
+            return backgroundColor;
+        }
+
+        public int collideWith(Collidable other, CollisionResults results) throws UnsupportedCollisionException {
+            return 0;
+        }
+
+        @Override
+        public void updateModelBound() {
+        }
+
+        @Override
+        public void setModelBound(BoundingVolume modelBound) {
+        }
+
+        @Override
+        public int getVertexCount() {
+            return 0;
+        }
+
+        @Override
+        public int getTriangleCount() {
+            return 0;
+        }
+
+        @Override
+        public Spatial deepClone() {
+            return null;
+        }
+
+        @Override
+        public void depthFirstTraversal(SceneGraphVisitor visitor) {
+        }
+
+        @Override
+        protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
+        }
+    }
+}

+ 69 - 0
jme3-blender/src/main/java/com/jme3/asset/GeneratedTextureKey.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2009-2012 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.asset;
+
+/**
+ * This key is mostly used to distinguish between textures that are loaded from
+ * the given assets and those being generated automatically. Every generated
+ * texture will have this kind of key attached.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class GeneratedTextureKey extends TextureKey {
+
+    /**
+     * Constructor. Stores the name. Extension and folder name are empty
+     * strings.
+     * 
+     * @param name
+     *            the name of the texture
+     */
+    public GeneratedTextureKey(String name) {
+        super(name);
+    }
+
+    @Override
+    public String getExtension() {
+        return "";
+    }
+
+    @Override
+    public String getFolder() {
+        return "";
+    }
+
+    @Override
+    public String toString() {
+        return "Generated texture [" + name + "]";
+    }
+}

+ 132 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java

@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2009-2012 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.blender;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.jme3.export.Savable;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.objects.Properties;
+
+/**
+ * A purpose of the helper class is to split calculation code into several classes. Each helper after use should be cleared because it can
+ * hold the state of the calculations.
+ * @author Marcin Roguski
+ */
+public abstract class AbstractBlenderHelper {
+    /** The blender context. */
+    protected BlenderContext blenderContext;
+    /** The version of the blend file. */
+    protected final int  blenderVersion;
+    /** This variable indicates if the Y asxis is the UP axis or not. */
+    protected boolean    fixUpAxis;
+    /** Quaternion used to rotate data when Y is up axis. */
+    protected Quaternion upAxisRotationQuaternion;
+
+    /**
+     * This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
+     * versions.
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public AbstractBlenderHelper(String blenderVersion, BlenderContext blenderContext) {
+        this.blenderVersion = Integer.parseInt(blenderVersion);
+        this.blenderContext = blenderContext;
+        fixUpAxis = blenderContext.getBlenderKey().isFixUpAxis();
+        if (fixUpAxis) {
+            upAxisRotationQuaternion = new Quaternion().fromAngles(-FastMath.HALF_PI, 0, 0);
+        }
+    }
+
+    /**
+     * This method loads the properties if they are available and defined for the structure.
+     * @param structure
+     *            the structure we read the properties from
+     * @param blenderContext
+     *            the blender context
+     * @return loaded properties or null if they are not available
+     * @throws BlenderFileException
+     *             an exception is thrown when the blend file is somehow corrupted
+     */
+    protected Properties loadProperties(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
+        Properties properties = null;
+        Structure id = (Structure) structure.getFieldValue("ID");
+        if (id != null) {
+            Pointer pProperties = (Pointer) id.getFieldValue("properties");
+            if (pProperties.isNotNull()) {
+                Structure propertiesStructure = pProperties.fetchData().get(0);
+                properties = new Properties();
+                properties.load(propertiesStructure, blenderContext);
+            }
+        }
+        return properties;
+    }
+
+    /**
+     * The method applies properties to the given spatial. The Properties
+     * instance cannot be directly applied because the end-user might not have
+     * the blender plugin jar file and thus receive ClassNotFoundException. The
+     * values are set by name instead.
+     * 
+     * @param spatial
+     *            the spatial that is to have properties applied
+     * @param properties
+     *            the properties to be applied
+     */
+    protected void applyProperties(Spatial spatial, Properties properties) {
+        List<String> propertyNames = properties.getSubPropertiesNames();
+        if (propertyNames != null && propertyNames.size() > 0) {
+            for (String propertyName : propertyNames) {
+                Object value = properties.findValue(propertyName);
+                if (value instanceof Savable || value instanceof Boolean || value instanceof String || value instanceof Float || value instanceof Integer || value instanceof Long) {
+                    spatial.setUserData(propertyName, value);
+                } else if (value instanceof Double) {
+                    spatial.setUserData(propertyName, ((Double) value).floatValue());
+                } else if (value instanceof int[]) {
+                    spatial.setUserData(propertyName, Arrays.toString((int[]) value));
+                } else if (value instanceof float[]) {
+                    spatial.setUserData(propertyName, Arrays.toString((float[]) value));
+                } else if (value instanceof double[]) {
+                    spatial.setUserData(propertyName, Arrays.toString((double[]) value));
+                }
+            }
+        }
+    }
+}

+ 636 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java

@@ -0,0 +1,636 @@
+/*
+ * Copyright (c) 2009-2012 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.blender;
+
+import java.util.ArrayList;
+import java.util.EmptyStackException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Stack;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.BlenderKey;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.Node;
+import com.jme3.scene.plugins.blender.animations.AnimationData;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.constraints.Constraint;
+import com.jme3.scene.plugins.blender.file.BlenderInputStream;
+import com.jme3.scene.plugins.blender.file.DnaBlockData;
+import com.jme3.scene.plugins.blender.file.FileBlockHeader;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.meshes.MeshContext;
+
+/**
+ * The class that stores temporary data and manages it during loading the belnd
+ * file. This class is intended to be used in a single loading thread. It holds
+ * the state of loading operations.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class BlenderContext {
+    /** The blender file version. */
+    private int                                 blenderVersion;
+    /** The blender key. */
+    private BlenderKey                          blenderKey;
+    /** The header of the file block. */
+    private DnaBlockData                        dnaBlockData;
+    /** The scene structure. */
+    private Structure                           sceneStructure;
+    /** The input stream of the blend file. */
+    private BlenderInputStream                  inputStream;
+    /** The asset manager. */
+    private AssetManager                        assetManager;
+    /** The blocks read from the file. */
+    protected List<FileBlockHeader> blocks;
+    /**
+     * A map containing the file block headers. The key is the old memory address.
+     */
+    private Map<Long, FileBlockHeader>          fileBlockHeadersByOma  = new HashMap<Long, FileBlockHeader>();
+    /** A map containing the file block headers. The key is the block code. */
+    private Map<Integer, List<FileBlockHeader>> fileBlockHeadersByCode = new HashMap<Integer, List<FileBlockHeader>>();
+    /**
+     * This map stores the loaded features by their old memory address. The
+     * first object in the value table is the loaded structure and the second -
+     * the structure already converted into proper data.
+     */
+    private Map<Long, Object[]>                 loadedFeatures         = new HashMap<Long, Object[]>();
+    /**
+     * This map stores the loaded features by their name. Only features with ID
+     * structure can be stored here. The first object in the value table is the
+     * loaded structure and the second - the structure already converted into
+     * proper data.
+     */
+    private Map<String, Object[]>               loadedFeaturesByName   = new HashMap<String, Object[]>();
+    /** A stack that hold the parent structure of currently loaded feature. */
+    private Stack<Structure>                    parentStack            = new Stack<Structure>();
+    /** A list of constraints for the specified object. */
+    protected Map<Long, List<Constraint>>       constraints            = new HashMap<Long, List<Constraint>>();
+    /** Anim data loaded for features. */
+    private Map<Long, AnimationData>            animData               = new HashMap<Long, AnimationData>();
+    /** Loaded skeletons. */
+    private Map<Long, Skeleton>                 skeletons              = new HashMap<Long, Skeleton>();
+    /** A map between skeleton and node it modifies. */
+    private Map<Skeleton, Node>                 nodesWithSkeletons     = new HashMap<Skeleton, Node>();
+    /** A map of mesh contexts. */
+    protected Map<Long, MeshContext>            meshContexts           = new HashMap<Long, MeshContext>();
+    /** A map of bone contexts. */
+    protected Map<Long, BoneContext>            boneContexts           = new HashMap<Long, BoneContext>();
+    /** A map og helpers that perform loading. */
+    private Map<String, AbstractBlenderHelper>  helpers                = new HashMap<String, AbstractBlenderHelper>();
+    /** Markers used by loading classes to store some custom data. This is made to avoid putting this data into user properties. */
+    private Map<String, Map<Object, Object>>    markers                = new HashMap<String, Map<Object, Object>>();
+
+    /**
+     * This method sets the blender file version.
+     * 
+     * @param blenderVersion
+     *            the blender file version
+     */
+    public void setBlenderVersion(String blenderVersion) {
+        this.blenderVersion = Integer.parseInt(blenderVersion);
+    }
+
+    /**
+     * @return the blender file version
+     */
+    public int getBlenderVersion() {
+        return blenderVersion;
+    }
+
+    /**
+     * This method sets the blender key.
+     * 
+     * @param blenderKey
+     *            the blender key
+     */
+    public void setBlenderKey(BlenderKey blenderKey) {
+        this.blenderKey = blenderKey;
+    }
+
+    /**
+     * This method returns the blender key.
+     * 
+     * @return the blender key
+     */
+    public BlenderKey getBlenderKey() {
+        return blenderKey;
+    }
+
+    /**
+     * This method sets the dna block data.
+     * 
+     * @param dnaBlockData
+     *            the dna block data
+     */
+    public void setBlockData(DnaBlockData dnaBlockData) {
+        this.dnaBlockData = dnaBlockData;
+    }
+
+    /**
+     * This method returns the dna block data.
+     * 
+     * @return the dna block data
+     */
+    public DnaBlockData getDnaBlockData() {
+        return dnaBlockData;
+    }
+
+    /**
+     * This method sets the scene structure data.
+     * 
+     * @param sceneStructure
+     *            the scene structure data
+     */
+    public void setSceneStructure(Structure sceneStructure) {
+        this.sceneStructure = sceneStructure;
+    }
+
+    /**
+     * This method returns the scene structure data.
+     * 
+     * @return the scene structure data
+     */
+    public Structure getSceneStructure() {
+        return sceneStructure;
+    }
+
+    /**
+     * This method returns the asset manager.
+     * 
+     * @return the asset manager
+     */
+    public AssetManager getAssetManager() {
+        return assetManager;
+    }
+
+    /**
+     * This method sets the asset manager.
+     * 
+     * @param assetManager
+     *            the asset manager
+     */
+    public void setAssetManager(AssetManager assetManager) {
+        this.assetManager = assetManager;
+    }
+
+    /**
+     * This method returns the input stream of the blend file.
+     * 
+     * @return the input stream of the blend file
+     */
+    public BlenderInputStream getInputStream() {
+        return inputStream;
+    }
+
+    /**
+     * This method sets the input stream of the blend file.
+     * 
+     * @param inputStream
+     *            the input stream of the blend file
+     */
+    public void setInputStream(BlenderInputStream inputStream) {
+        this.inputStream = inputStream;
+    }
+
+    /**
+     * This method adds a file block header to the map. Its old memory address
+     * is the key.
+     * 
+     * @param oldMemoryAddress
+     *            the address of the block header
+     * @param fileBlockHeader
+     *            the block header to store
+     */
+    public void addFileBlockHeader(Long oldMemoryAddress, FileBlockHeader fileBlockHeader) {
+        fileBlockHeadersByOma.put(oldMemoryAddress, fileBlockHeader);
+        List<FileBlockHeader> headers = fileBlockHeadersByCode.get(Integer.valueOf(fileBlockHeader.getCode()));
+        if (headers == null) {
+            headers = new ArrayList<FileBlockHeader>();
+            fileBlockHeadersByCode.put(Integer.valueOf(fileBlockHeader.getCode()), headers);
+        }
+        headers.add(fileBlockHeader);
+    }
+
+    /**
+     * This method returns the block header of a given memory address. If the
+     * header is not present then null is returned.
+     * 
+     * @param oldMemoryAddress
+     *            the address of the block header
+     * @return loaded header or null if it was not yet loaded
+     */
+    public FileBlockHeader getFileBlock(Long oldMemoryAddress) {
+        return fileBlockHeadersByOma.get(oldMemoryAddress);
+    }
+
+    /**
+     * This method returns a list of file blocks' headers of a specified code.
+     * 
+     * @param code
+     *            the code of file blocks
+     * @return a list of file blocks' headers of a specified code
+     */
+    public List<FileBlockHeader> getFileBlocks(Integer code) {
+        return fileBlockHeadersByCode.get(code);
+    }
+
+    /**
+     * This method adds a helper instance to the helpers' map.
+     * 
+     * @param <T>
+     *            the type of the helper
+     * @param clazz
+     *            helper's class definition
+     * @param helper
+     *            the helper instance
+     */
+    public <T> void putHelper(Class<T> clazz, AbstractBlenderHelper helper) {
+        helpers.put(clazz.getSimpleName(), helper);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T getHelper(Class<?> clazz) {
+        return (T) helpers.get(clazz.getSimpleName());
+    }
+
+    /**
+     * This method adds a loaded feature to the map. The key is its unique old
+     * memory address.
+     * 
+     * @param oldMemoryAddress
+     *            the address of the feature
+     * @param featureName
+     *            the name of the feature
+     * @param structure
+     *            the filled structure of the feature
+     * @param feature
+     *            the feature we want to store
+     */
+    public void addLoadedFeatures(Long oldMemoryAddress, String featureName, Structure structure, Object feature) {
+        if (oldMemoryAddress == null || structure == null || feature == null) {
+            throw new IllegalArgumentException("One of the given arguments is null!");
+        }
+        Object[] storedData = new Object[] { structure, feature };
+        loadedFeatures.put(oldMemoryAddress, storedData);
+        if (featureName != null) {
+            loadedFeaturesByName.put(featureName, storedData);
+        }
+    }
+
+    /**
+     * This method returns the feature of a given memory address. If the feature
+     * is not yet loaded then null is returned.
+     * 
+     * @param oldMemoryAddress
+     *            the address of the feature
+     * @param loadedFeatureDataType
+     *            the type of data we want to retreive it can be either filled
+     *            structure or already converted feature
+     * @return loaded feature or null if it was not yet loaded
+     */
+    public Object getLoadedFeature(Long oldMemoryAddress, LoadedFeatureDataType loadedFeatureDataType) {
+        Object[] result = loadedFeatures.get(oldMemoryAddress);
+        if (result != null) {
+            return result[loadedFeatureDataType.getIndex()];
+        }
+        return null;
+    }
+
+    /**
+     * This method adds the structure to the parent stack.
+     * 
+     * @param parent
+     *            the structure to be added to the stack
+     */
+    public void pushParent(Structure parent) {
+        parentStack.push(parent);
+    }
+
+    /**
+     * This method removes the structure from the top of the parent's stack.
+     * 
+     * @return the structure that was removed from the stack
+     */
+    public Structure popParent() {
+        try {
+            return parentStack.pop();
+        } catch (EmptyStackException e) {
+            return null;
+        }
+    }
+
+    /**
+     * This method retreives the structure at the top of the parent's stack but
+     * does not remove it.
+     * 
+     * @return the structure from the top of the stack
+     */
+    public Structure peekParent() {
+        try {
+            return parentStack.peek();
+        } catch (EmptyStackException e) {
+            return null;
+        }
+    }
+
+    /**
+     * This method adds a new modifier to the list.
+     * 
+     * @param ownerOMA
+     *            the owner's old memory address
+     * @param constraints
+     *            the object's constraints
+     */
+    public void addConstraints(Long ownerOMA, List<Constraint> constraints) {
+        List<Constraint> objectConstraints = this.constraints.get(ownerOMA);
+        if (objectConstraints == null) {
+            objectConstraints = new ArrayList<Constraint>();
+            this.constraints.put(ownerOMA, objectConstraints);
+        }
+        objectConstraints.addAll(constraints);
+    }
+
+    /**
+     * Returns constraints applied to the feature of the given OMA.
+     * @param ownerOMA
+     *            the constraints' owner OMA
+     * @return a list of constraints or <b>null</b> if no constraints are applied to the feature
+     */
+    public List<Constraint> getConstraints(Long ownerOMA) {
+        return constraints.get(ownerOMA);
+    }
+
+    /**
+     * @return all available constraints
+     */
+    public List<Constraint> getAllConstraints() {
+        List<Constraint> result = new ArrayList<Constraint>();
+        for (Entry<Long, List<Constraint>> entry : constraints.entrySet()) {
+            result.addAll(entry.getValue());
+        }
+        return result;
+    }
+
+    /**
+     * This method sets the anim data for the specified OMA of its owner.
+     * 
+     * @param ownerOMA
+     *            the owner's old memory address
+     * @param animData
+     *            the animation data for the feature specified by ownerOMA
+     */
+    public void setAnimData(Long ownerOMA, AnimationData animData) {
+        this.animData.put(ownerOMA, animData);
+    }
+
+    /**
+     * This method returns the animation data for the specified owner.
+     * 
+     * @param ownerOMA
+     *            the old memory address of the animation data owner
+     * @return the animation data or null if none exists
+     */
+    public AnimationData getAnimData(Long ownerOMA) {
+        return animData.get(ownerOMA);
+    }
+
+    /**
+     * This method sets the skeleton for the specified OMA of its owner.
+     * 
+     * @param skeletonOMA
+     *            the skeleton's old memory address
+     * @param skeleton
+     *            the skeleton specified by the given OMA
+     */
+    public void setSkeleton(Long skeletonOMA, Skeleton skeleton) {
+        skeletons.put(skeletonOMA, skeleton);
+    }
+
+    /**
+     * The method stores a binding between the skeleton and the proper armature
+     * node.
+     * 
+     * @param skeleton
+     *            the skeleton
+     * @param node
+     *            the armature node
+     */
+    public void setNodeForSkeleton(Skeleton skeleton, Node node) {
+        nodesWithSkeletons.put(skeleton, node);
+    }
+
+    /**
+     * This method returns the armature node that is defined for the skeleton.
+     * 
+     * @param skeleton
+     *            the skeleton
+     * @return the armature node that defines the skeleton in blender
+     */
+    public Node getControlledNode(Skeleton skeleton) {
+        return nodesWithSkeletons.get(skeleton);
+    }
+
+    /**
+     * This method returns the skeleton for the specified OMA of its owner.
+     * 
+     * @param skeletonOMA
+     *            the skeleton's old memory address
+     * @return the skeleton specified by the given OMA
+     */
+    public Skeleton getSkeleton(Long skeletonOMA) {
+        return skeletons.get(skeletonOMA);
+    }
+
+    /**
+     * This method sets the mesh context for the given mesh old memory address.
+     * If the context is already set it will be replaced.
+     * 
+     * @param meshOMA
+     *            the mesh's old memory address
+     * @param meshContext
+     *            the mesh's context
+     */
+    public void setMeshContext(Long meshOMA, MeshContext meshContext) {
+        meshContexts.put(meshOMA, meshContext);
+    }
+
+    /**
+     * This method returns the mesh context for the given mesh old memory
+     * address. If no context exists then <b>null</b> is returned.
+     * 
+     * @param meshOMA
+     *            the mesh's old memory address
+     * @return mesh's context
+     */
+    public MeshContext getMeshContext(Long meshOMA) {
+        return meshContexts.get(meshOMA);
+    }
+
+    /**
+     * This method sets the bone context for the given bone old memory address.
+     * If the context is already set it will be replaced.
+     * 
+     * @param boneOMA
+     *            the bone's old memory address
+     * @param boneContext
+     *            the bones's context
+     */
+    public void setBoneContext(Long boneOMA, BoneContext boneContext) {
+        boneContexts.put(boneOMA, boneContext);
+    }
+
+    /**
+     * This method returns the bone context for the given bone old memory
+     * address. If no context exists then <b>null</b> is returned.
+     * 
+     * @param boneOMA
+     *            the bone's old memory address
+     * @return bone's context
+     */
+    public BoneContext getBoneContext(Long boneOMA) {
+        return boneContexts.get(boneOMA);
+    }
+
+    /**
+     * Returns bone by given name.
+     * 
+     * @param skeletonOMA the OMA of the skeleton where the bone will be searched
+     * @param name
+     *            the name of the bone
+     * @return found bone or null if none bone of a given name exists
+     */
+    public BoneContext getBoneByName(Long skeletonOMA, String name) {
+        for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) {
+            if(entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) {
+                Bone bone = entry.getValue().getBone();
+                if (bone != null && name.equals(bone.getName())) {
+                    return entry.getValue();
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns bone context for the given bone.
+     * 
+     * @param bone
+     *            the bone
+     * @return the bone's bone context
+     */
+    public BoneContext getBoneContext(Bone bone) {
+        for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) {
+            if (entry.getValue().getBone().equals(bone)) {
+                return entry.getValue();
+            }
+        }
+        throw new IllegalStateException("Cannot find context for bone: " + bone);
+    }
+
+    /**
+     * This metod returns the default material.
+     * 
+     * @return the default material
+     */
+    public synchronized Material getDefaultMaterial() {
+        if (blenderKey.getDefaultMaterial() == null) {
+            Material defaultMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+            defaultMaterial.setColor("Color", ColorRGBA.DarkGray);
+            blenderKey.setDefaultMaterial(defaultMaterial);
+        }
+        return blenderKey.getDefaultMaterial();
+    }
+
+    /**
+     * Adds a custom marker for scene's feature.
+     * 
+     * @param marker
+     *            the marker name
+     * @param feature
+     *            te scene's feature (can be node, material or texture or
+     *            anything else)
+     * @param markerValue
+     *            the marker value
+     */
+    public void addMarker(String marker, Object feature, Object markerValue) {
+        if (markerValue == null) {
+            throw new IllegalArgumentException("The marker's value cannot be null.");
+        }
+        Map<Object, Object> markersMap = markers.get(marker);
+        if (markersMap == null) {
+            markersMap = new HashMap<Object, Object>();
+            markers.put(marker, markersMap);
+        }
+        markersMap.put(feature, markerValue);
+    }
+
+    /**
+     * Returns the marker value. The returned value is null if no marker was
+     * defined for the given feature.
+     * 
+     * @param marker
+     *            the marker name
+     * @param feature
+     *            the scene's feature
+     * @return marker value or null if it was not defined
+     */
+    public Object getMarkerValue(String marker, Object feature) {
+        Map<Object, Object> markersMap = markers.get(marker);
+        return markersMap == null ? null : markersMap.get(feature);
+    }
+
+    /**
+     * This enum defines what loaded data type user wants to retreive. It can be
+     * either filled structure or already converted data.
+     * 
+     * @author Marcin Roguski (Kaelthas)
+     */
+    public static enum LoadedFeatureDataType {
+
+        LOADED_STRUCTURE(0), LOADED_FEATURE(1);
+        private int index;
+
+        private LoadedFeatureDataType(int index) {
+            this.index = index;
+        }
+
+        public int getIndex() {
+            return index;
+        }
+    }
+}

+ 282 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java

@@ -0,0 +1,282 @@
+/*
+ * Copyright (c) 2009-2012 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.blender;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.BlenderKey;
+import com.jme3.asset.BlenderKey.FeaturesToLoad;
+import com.jme3.asset.BlenderKey.LoadingResults;
+import com.jme3.asset.ModelKey;
+import com.jme3.scene.CameraNode;
+import com.jme3.scene.LightNode;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
+import com.jme3.scene.plugins.blender.animations.IpoHelper;
+import com.jme3.scene.plugins.blender.cameras.CameraHelper;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
+import com.jme3.scene.plugins.blender.curves.CurvesHelper;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.BlenderInputStream;
+import com.jme3.scene.plugins.blender.file.FileBlockHeader;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.landscape.LandscapeHelper;
+import com.jme3.scene.plugins.blender.lights.LightHelper;
+import com.jme3.scene.plugins.blender.materials.MaterialHelper;
+import com.jme3.scene.plugins.blender.meshes.MeshHelper;
+import com.jme3.scene.plugins.blender.modifiers.ModifierHelper;
+import com.jme3.scene.plugins.blender.objects.ObjectHelper;
+import com.jme3.scene.plugins.blender.particles.ParticlesHelper;
+import com.jme3.scene.plugins.blender.textures.TextureHelper;
+
+/**
+ * This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures.
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class BlenderLoader implements AssetLoader {
+    private static final Logger     LOGGER = Logger.getLogger(BlenderLoader.class.getName());
+
+    /** The blocks read from the file. */
+    protected List<FileBlockHeader> blocks;
+    /** The blender context. */
+    protected BlenderContext        blenderContext;
+
+    public Spatial load(AssetInfo assetInfo) throws IOException {
+        try {
+            this.setup(assetInfo);
+
+            List<FileBlockHeader> sceneBlocks = new ArrayList<FileBlockHeader>();
+            BlenderKey blenderKey = blenderContext.getBlenderKey();
+            LoadingResults loadingResults = blenderKey.prepareLoadingResults();
+            for (FileBlockHeader block : blocks) {
+                switch (block.getCode()) {
+                    case FileBlockHeader.BLOCK_OB00:// Object
+                        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
+                        Object object = objectHelper.toObject(block.getStructure(blenderContext), blenderContext);
+                        if (object instanceof LightNode) {
+                            loadingResults.addLight((LightNode) object);
+                        } else if (object instanceof CameraNode) {
+                            loadingResults.addCamera((CameraNode) object);
+                        } else if (object instanceof Node) {
+                            if (LOGGER.isLoggable(Level.FINE)) {
+                                LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
+                            }
+                            if (this.isRootObject(loadingResults, (Node) object)) {
+                                loadingResults.addObject((Node) object);
+                            }
+                        }
+                        break;
+//                    case FileBlockHeader.BLOCK_MA00:// Material
+//                        MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
+//                        MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext);
+//                        if (blenderKey.isLoadUnlinkedAssets() && blenderKey.shouldLoad(FeaturesToLoad.MATERIALS)) {
+//                            loadingResults.addMaterial(this.toMaterial(block.getStructure(blenderContext)));
+//                        }
+//                        break;
+                    case FileBlockHeader.BLOCK_SC00:// Scene
+                        if (blenderKey.shouldLoad(FeaturesToLoad.SCENES)) {
+                            sceneBlocks.add(block);
+                        }
+                        break;
+                    case FileBlockHeader.BLOCK_WO00:// World
+                        if (blenderKey.shouldLoad(FeaturesToLoad.WORLD)) {
+                            Structure worldStructure = block.getStructure(blenderContext);
+                            String worldName = worldStructure.getName();
+                            if (blenderKey.getUsedWorld() == null || blenderKey.getUsedWorld().equals(worldName)) {
+                                LandscapeHelper landscapeHelper = blenderContext.getHelper(LandscapeHelper.class);
+                                loadingResults.addLight(landscapeHelper.toAmbientLight(worldStructure));
+                                loadingResults.setSky(landscapeHelper.toSky(worldStructure));
+                                loadingResults.setBackgroundColor(landscapeHelper.toBackgroundColor(worldStructure));
+                            }
+                        }
+                        break;
+                }
+            }
+
+            // bake constraints after everything is loaded
+            ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
+            constraintHelper.bakeConstraints(blenderContext);
+
+            // load the scene at the very end so that the root nodes have no parent during loading or constraints applying
+            for (FileBlockHeader sceneBlock : sceneBlocks) {
+                loadingResults.addScene(this.toScene(sceneBlock.getStructure(blenderContext)));
+            }
+
+            return loadingResults;
+        } catch (BlenderFileException e) {
+            throw new IOException(e.getLocalizedMessage(), e);
+        } catch (Exception e) {
+            throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e);
+        } finally {
+            this.clear();
+        }
+    }
+
+    /**
+     * This method indicates if the given spatial is a root object. It means it
+     * has no parent or is directly attached to one of the already loaded scene
+     * nodes.
+     * 
+     * @param loadingResults
+     *            loading results containing the scene nodes
+     * @param spatial
+     *            spatial object
+     * @return <b>true</b> if the given spatial is a root object and
+     *         <b>false</b> otherwise
+     */
+    protected boolean isRootObject(LoadingResults loadingResults, Spatial spatial) {
+        if (spatial.getParent() == null) {
+            return true;
+        }
+        for (Node scene : loadingResults.getScenes()) {
+            if (spatial.getParent().equals(scene)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * This method converts the given structure to a scene node.
+     * @param structure
+     *            structure of a scene
+     * @return scene's node
+     */
+    private Node toScene(Structure structure) {
+        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
+        Node result = new Node(structure.getName());
+        try {
+            List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase();
+            for (Structure b : base) {
+                Pointer pObject = (Pointer) b.getFieldValue("object");
+                if (pObject.isNotNull()) {
+                    Structure objectStructure = pObject.fetchData().get(0);
+
+                    Object object = objectHelper.toObject(objectStructure, blenderContext);
+                    if (object instanceof LightNode) {
+                        result.addLight(((LightNode) object).getLight());
+                        result.attachChild((LightNode) object);
+                    } else if (object instanceof Node) {
+                        if (LOGGER.isLoggable(Level.FINE)) {
+                            LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
+                        }
+                        if (((Node) object).getParent() == null) {
+                            result.attachChild((Spatial) object);
+                        }
+                    }
+                }
+            }
+        } catch (BlenderFileException e) {
+            LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
+        }
+        return result;
+    }
+
+    /**
+     * This method sets up the loader.
+     * @param assetInfo
+     *            the asset info
+     * @throws BlenderFileException
+     *             an exception is throw when something wrong happens with blender file
+     */
+    protected void setup(AssetInfo assetInfo) throws BlenderFileException {
+        // registering loaders
+        ModelKey modelKey = (ModelKey) assetInfo.getKey();
+        BlenderKey blenderKey;
+        if (modelKey instanceof BlenderKey) {
+            blenderKey = (BlenderKey) modelKey;
+        } else {
+            blenderKey = new BlenderKey(modelKey.getName());
+            blenderKey.setAssetRootPath(modelKey.getFolder());
+        }
+
+        // opening stream
+        BlenderInputStream inputStream = new BlenderInputStream(assetInfo.openStream());
+
+        // reading blocks
+        blocks = new ArrayList<FileBlockHeader>();
+        FileBlockHeader fileBlock;
+        blenderContext = new BlenderContext();
+        blenderContext.setBlenderVersion(inputStream.getVersionNumber());
+        blenderContext.setAssetManager(assetInfo.getManager());
+        blenderContext.setInputStream(inputStream);
+        blenderContext.setBlenderKey(blenderKey);
+
+        // creating helpers
+        blenderContext.putHelper(ArmatureHelper.class, new ArmatureHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(TextureHelper.class, new TextureHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(MeshHelper.class, new MeshHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(ObjectHelper.class, new ObjectHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(CurvesHelper.class, new CurvesHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(LightHelper.class, new LightHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(CameraHelper.class, new CameraHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(ModifierHelper.class, new ModifierHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(MaterialHelper.class, new MaterialHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(IpoHelper.class, new IpoHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext));
+        blenderContext.putHelper(LandscapeHelper.class, new LandscapeHelper(inputStream.getVersionNumber(), blenderContext));
+
+        // reading the blocks (dna block is automatically saved in the blender context when found)
+        FileBlockHeader sceneFileBlock = null;
+        do {
+            fileBlock = new FileBlockHeader(inputStream, blenderContext);
+            if (!fileBlock.isDnaBlock()) {
+                blocks.add(fileBlock);
+                // save the scene's file block
+                if (fileBlock.getCode() == FileBlockHeader.BLOCK_SC00) {
+                    sceneFileBlock = fileBlock;
+                }
+            }
+        } while (!fileBlock.isLastBlock());
+        if (sceneFileBlock != null) {
+            blenderContext.setSceneStructure(sceneFileBlock.getStructure(blenderContext));
+        }
+    }
+
+    /**
+     * The internal data is only needed during loading so make it unreachable so that the GC can release
+     * that memory (which can be quite large amount).
+     */
+    protected void clear() {
+        blenderContext = null;
+        blocks = null;
+    }
+}

+ 105 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderModelLoader.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2009-2012 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.blender;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.BlenderKey;
+import com.jme3.asset.BlenderKey.FeaturesToLoad;
+import com.jme3.scene.LightNode;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.FileBlockHeader;
+import com.jme3.scene.plugins.blender.objects.ObjectHelper;
+
+/**
+ * This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class BlenderModelLoader extends BlenderLoader {
+
+    private static final Logger LOGGER = Logger.getLogger(BlenderModelLoader.class.getName());
+
+    @Override
+    public Spatial load(AssetInfo assetInfo) throws IOException {
+        try {
+            this.setup(assetInfo);
+
+            BlenderKey blenderKey = blenderContext.getBlenderKey();
+            List<Node> rootObjects = new ArrayList<Node>();
+            for (FileBlockHeader block : blocks) {
+                if (block.getCode() == FileBlockHeader.BLOCK_OB00) {
+                    ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
+                    Object object = objectHelper.toObject(block.getStructure(blenderContext), blenderContext);
+                    if (object instanceof LightNode && (blenderKey.getFeaturesToLoad() & FeaturesToLoad.LIGHTS) != 0) {
+                        rootObjects.add((LightNode) object);
+                    } else if (object instanceof Node && (blenderKey.getFeaturesToLoad() & FeaturesToLoad.OBJECTS) != 0) {
+                        LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
+                        if (((Node) object).getParent() == null) {
+                            rootObjects.add((Node) object);
+                        }
+                    }
+                }
+            }
+
+            // bake constraints after everything is loaded
+            ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
+            constraintHelper.bakeConstraints(blenderContext);
+
+            // attach the nodes to the root node at the very end so that the root objects have no parents during constraint applying
+            LOGGER.fine("Creating the root node of the model and applying loaded nodes of the scene to it.");
+            Node modelRoot = new Node(blenderKey.getName());
+            for (Node node : rootObjects) {
+                if (node instanceof LightNode) {
+                    modelRoot.addLight(((LightNode) node).getLight());
+                }
+                modelRoot.attachChild(node);
+            }
+
+            return modelRoot;
+        } catch (BlenderFileException e) {
+            throw new IOException(e.getLocalizedMessage(), e);
+        } catch (Exception e) {
+            throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e);
+        } finally {
+            this.clear();
+        }
+    }
+}

+ 29 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationData.java

@@ -0,0 +1,29 @@
+package com.jme3.scene.plugins.blender.animations;
+
+import java.util.List;
+
+import com.jme3.animation.Animation;
+import com.jme3.animation.Skeleton;
+
+/**
+ * A simple class that sotres animation data.
+ * If skeleton is null then we deal with object animation.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class AnimationData {
+    /** The skeleton. */
+    public final Skeleton skeleton;
+    /** The animations list. */
+    public final List<Animation> anims;
+
+    public AnimationData(List<Animation> anims) {
+        this.anims = anims;
+        skeleton = null;
+    }
+    
+    public AnimationData(Skeleton skeleton, List<Animation> anims) {
+        this.skeleton = skeleton;
+        this.anims = anims;
+    }
+}

+ 276 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/ArmatureHelper.java

@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.animations;
+
+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;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.BoneTrack;
+import com.jme3.animation.Skeleton;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.curves.BezierCurve;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class defines the methods to calculate certain aspects of animation and
+ * armature functionalities.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class ArmatureHelper extends AbstractBlenderHelper {
+    private static final Logger LOGGER               = Logger.getLogger(ArmatureHelper.class.getName());
+
+    public static final String  ARMATURE_NODE_MARKER = "armature-node";
+
+    /**
+     * This constructor parses the given blender version and stores the result.
+     * Some functionalities may differ in different blender versions.
+     * 
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public ArmatureHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    /**
+     * This method builds the object's bones structure.
+     * 
+     * @param armatureObjectOMA
+     *            the OMa of the armature node
+     * @param boneStructure
+     *            the structure containing the bones' data
+     * @param parent
+     *            the parent bone
+     * @param result
+     *            the list where the newly created bone will be added
+     * @param spatialOMA
+     *            the OMA of the spatial that will own the skeleton
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             an exception is thrown when there is problem with the blender
+     *             file
+     */
+    public void buildBones(Long armatureObjectOMA, Structure boneStructure, Bone parent, List<Bone> result, Long spatialOMA, BlenderContext blenderContext) throws BlenderFileException {
+        BoneContext bc = new BoneContext(armatureObjectOMA, boneStructure, blenderContext);
+        bc.buildBone(result, spatialOMA, blenderContext);
+    }
+
+    /**
+     * This method returns a map where the key is the object's group index that
+     * is used by a bone and the key is the bone index in the armature.
+     * 
+     * @param defBaseStructure
+     *            a bPose structure of the object
+     * @return bone group-to-index map
+     * @throws BlenderFileException
+     *             this exception is thrown when the blender file is somehow
+     *             corrupted
+     */
+    public Map<Integer, Integer> getGroupToBoneIndexMap(Structure defBaseStructure, Skeleton skeleton) throws BlenderFileException {
+        Map<Integer, Integer> result = null;
+        if (skeleton.getBoneCount() != 0) {
+            result = new HashMap<Integer, Integer>();
+            List<Structure> deformGroups = defBaseStructure.evaluateListBase();// bDeformGroup
+            int groupIndex = 0;
+            for (Structure deformGroup : deformGroups) {
+                String deformGroupName = deformGroup.getFieldValue("name").toString();
+                int boneIndex = skeleton.getBoneIndex(deformGroupName);
+                if (boneIndex >= 0) {
+                    result.put(groupIndex, boneIndex);
+                }
+                ++groupIndex;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * This method retuns the bone tracks for animation.
+     * 
+     * @param actionStructure
+     *            the structure containing the tracks
+     * @param blenderContext
+     *            the blender context
+     * @return a list of tracks for the specified animation
+     * @throws BlenderFileException
+     *             an exception is thrown when there are problems with the blend
+     *             file
+     */
+    public BoneTrack[] getTracks(Structure actionStructure, Skeleton skeleton, BlenderContext blenderContext) throws BlenderFileException {
+        if (blenderVersion < 250) {
+            return this.getTracks249(actionStructure, skeleton, blenderContext);
+        } else {
+            return this.getTracks250(actionStructure, skeleton, blenderContext);
+        }
+    }
+
+    /**
+     * This method retuns the bone tracks for animation for blender version 2.50
+     * and higher.
+     * 
+     * @param actionStructure
+     *            the structure containing the tracks
+     * @param blenderContext
+     *            the blender context
+     * @return a list of tracks for the specified animation
+     * @throws BlenderFileException
+     *             an exception is thrown when there are problems with the blend
+     *             file
+     */
+    private BoneTrack[] getTracks250(Structure actionStructure, Skeleton skeleton, BlenderContext blenderContext) throws BlenderFileException {
+        LOGGER.log(Level.FINE, "Getting tracks!");
+        IpoHelper ipoHelper = blenderContext.getHelper(IpoHelper.class);
+        int fps = blenderContext.getBlenderKey().getFps();
+        Structure groups = (Structure) actionStructure.getFieldValue("groups");
+        List<Structure> actionGroups = groups.evaluateListBase();// bActionGroup
+        List<BoneTrack> tracks = new ArrayList<BoneTrack>();
+        for (Structure actionGroup : actionGroups) {
+            String name = actionGroup.getFieldValue("name").toString();
+            int boneIndex = skeleton.getBoneIndex(name);
+            if (boneIndex >= 0) {
+                List<Structure> channels = ((Structure) actionGroup.getFieldValue("channels")).evaluateListBase();
+                BezierCurve[] bezierCurves = new BezierCurve[channels.size()];
+                int channelCounter = 0;
+                for (Structure c : channels) {
+                    int type = ipoHelper.getCurveType(c, blenderContext);
+                    Pointer pBezTriple = (Pointer) c.getFieldValue("bezt");
+                    List<Structure> bezTriples = pBezTriple.fetchData();
+                    bezierCurves[channelCounter++] = new BezierCurve(type, bezTriples, 2);
+                }
+
+                Bone bone = skeleton.getBone(boneIndex);
+                Ipo ipo = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
+                tracks.add((BoneTrack) ipo.calculateTrack(boneIndex, bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale(), 0, ipo.getLastFrame(), fps, false));
+            }
+        }
+        this.equaliseBoneTracks(tracks);
+        return tracks.toArray(new BoneTrack[tracks.size()]);
+    }
+
+    /**
+     * This method retuns the bone tracks for animation for blender version 2.49
+     * (and probably several lower versions too).
+     * 
+     * @param actionStructure
+     *            the structure containing the tracks
+     * @param blenderContext
+     *            the blender context
+     * @return a list of tracks for the specified animation
+     * @throws BlenderFileException
+     *             an exception is thrown when there are problems with the blend
+     *             file
+     */
+    private BoneTrack[] getTracks249(Structure actionStructure, Skeleton skeleton, BlenderContext blenderContext) throws BlenderFileException {
+        LOGGER.log(Level.FINE, "Getting tracks!");
+        IpoHelper ipoHelper = blenderContext.getHelper(IpoHelper.class);
+        int fps = blenderContext.getBlenderKey().getFps();
+        Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase");
+        List<Structure> actionChannels = chanbase.evaluateListBase();// bActionChannel
+        List<BoneTrack> tracks = new ArrayList<BoneTrack>();
+        for (Structure bActionChannel : actionChannels) {
+            String name = bActionChannel.getFieldValue("name").toString();
+            int boneIndex = skeleton.getBoneIndex(name);
+            if (boneIndex >= 0) {
+                Pointer p = (Pointer) bActionChannel.getFieldValue("ipo");
+                if (!p.isNull()) {
+                    Structure ipoStructure = p.fetchData().get(0);
+
+                    Bone bone = skeleton.getBone(boneIndex);
+                    Ipo ipo = ipoHelper.fromIpoStructure(ipoStructure, blenderContext);
+                    if (ipo != null) {
+                        tracks.add((BoneTrack) ipo.calculateTrack(boneIndex, bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale(), 0, ipo.getLastFrame(), fps, false));
+                    }
+                }
+            }
+        }
+        this.equaliseBoneTracks(tracks);
+        return tracks.toArray(new BoneTrack[tracks.size()]);
+    }
+
+    /**
+     * The method makes all the tracks to have equal frame lengths.
+     * @param tracks
+     *            the tracks to be equalized
+     */
+    private void equaliseBoneTracks(List<BoneTrack> tracks) {
+        // first compute the maximum amount of frames
+        int maximumFrameCount = -1;
+        float[] maximumTrackTimes = null;
+        for (BoneTrack track : tracks) {
+            if (track.getTimes().length > maximumFrameCount) {
+                maximumTrackTimes = track.getTimes();
+                maximumFrameCount = maximumTrackTimes.length;
+            }
+        }
+
+        // now widen all the tracks that have less frames by repeating the last values in the frame
+        for (BoneTrack track : tracks) {
+            int currentTrackLength = track.getTimes().length;
+            if (currentTrackLength < maximumFrameCount) {
+                Vector3f[] translations = new Vector3f[maximumFrameCount];
+                Quaternion[] rotations = new Quaternion[maximumFrameCount];
+                Vector3f[] scales = new Vector3f[maximumFrameCount];
+
+                Vector3f[] currentTranslations = track.getTranslations();
+                Quaternion[] currentRotations = track.getRotations();
+                Vector3f[] currentScales = track.getScales();
+                for (int i = 0; i < currentTrackLength; ++i) {
+                    translations[i] = currentTranslations[i];
+                    rotations[i] = currentRotations[i];
+                    scales[i] = currentScales[i];
+                }
+
+                for (int i = currentTrackLength; i < maximumFrameCount; ++i) {
+                    translations[i] = currentTranslations[currentTranslations.length - 1];
+                    rotations[i] = currentRotations[currentRotations.length - 1];
+                    scales[i] = currentScales[currentScales.length - 1];
+                }
+
+                track.setKeyframes(maximumTrackTimes, translations, rotations, scales);
+            }
+        }
+    }
+}

+ 223 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneContext.java

@@ -0,0 +1,223 @@
+package com.jme3.scene.plugins.blender.animations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.objects.ObjectHelper;
+
+/**
+ * This class holds the basic data that describes a bone.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class BoneContext {
+    // the flags of the bone
+    public static final int      CONNECTED_TO_PARENT                 = 0x10;
+
+    /**
+     * The bones' matrices have, unlike objects', the coordinate system identical to JME's (Y axis is UP, X to the right and Z toward us).
+     * So in order to have them loaded properly we need to transform their armature matrix (which blender sees as rotated) to make sure we get identical results.
+     */
+    public static final Matrix4f BONE_ARMATURE_TRANSFORMATION_MATRIX = new Matrix4f(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
+
+    private BlenderContext       blenderContext;
+    /** The OMA of the bone's armature object. */
+    private Long                 armatureObjectOMA;
+    /** The OMA of the model that owns the bone's skeleton. */
+    private Long                 skeletonOwnerOma;
+    /** The structure of the bone. */
+    private Structure            boneStructure;
+    /** Bone's name. */
+    private String               boneName;
+    /** The bone's flag. */
+    private int                  flag;
+    /** The bone's matrix in world space. */
+    private Matrix4f             globalBoneMatrix;
+    /** The bone's matrix in the model space. */
+    private Matrix4f             boneMatrixInModelSpace;
+    /** The parent context. */
+    private BoneContext          parent;
+    /** The children of this context. */
+    private List<BoneContext>    children                            = new ArrayList<BoneContext>();
+    /** Created bone (available after calling 'buildBone' method). */
+    private Bone                 bone;
+    /** The length of the bone. */
+    private float                length;
+
+    /**
+     * Constructor. Creates the basic set of bone's data.
+     * 
+     * @param armatureObjectOMA
+     *            the OMA of the bone's armature object
+     * @param boneStructure
+     *            the bone's structure
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             an exception is thrown when problem with blender data reading
+     *             occurs
+     */
+    public BoneContext(Long armatureObjectOMA, Structure boneStructure, BlenderContext blenderContext) throws BlenderFileException {
+        this(boneStructure, armatureObjectOMA, null, blenderContext);
+    }
+
+    /**
+     * Constructor. Creates the basic set of bone's data.
+     * 
+     * @param boneStructure
+     *            the bone's structure
+     * @param armatureObjectOMA
+     *            the OMA of the bone's armature object
+     * @param parent
+     *            bone's parent (null if the bone is the root bone)
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             an exception is thrown when problem with blender data reading
+     *             occurs
+     */
+    private BoneContext(Structure boneStructure, Long armatureObjectOMA, BoneContext parent, BlenderContext blenderContext) throws BlenderFileException {
+        this.parent = parent;
+        this.blenderContext = blenderContext;
+        this.boneStructure = boneStructure;
+        this.armatureObjectOMA = armatureObjectOMA;
+        boneName = boneStructure.getFieldValue("name").toString();
+        flag = ((Number) boneStructure.getFieldValue("flag")).intValue();
+        length = ((Number) boneStructure.getFieldValue("length")).floatValue();
+        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
+
+        // first get the bone matrix in its armature space
+        globalBoneMatrix = objectHelper.getMatrix(boneStructure, "arm_mat", blenderContext.getBlenderKey().isFixUpAxis());
+        // then make sure it is rotated in a proper way to fit the jme bone transformation conventions
+        globalBoneMatrix.multLocal(BONE_ARMATURE_TRANSFORMATION_MATRIX);
+
+        Spatial armature = (Spatial) objectHelper.toObject(blenderContext.getFileBlock(armatureObjectOMA).getStructure(blenderContext), blenderContext);
+        ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
+        Matrix4f armatureWorldMatrix = constraintHelper.toMatrix(armature.getWorldTransform(), new Matrix4f());
+
+        // and now compute the final bone matrix in world space
+        globalBoneMatrix = armatureWorldMatrix.mult(globalBoneMatrix);
+
+        // create the children
+        List<Structure> childbase = ((Structure) boneStructure.getFieldValue("childbase")).evaluateListBase();
+        for (Structure child : childbase) {
+            children.add(new BoneContext(child, armatureObjectOMA, this, blenderContext));
+        }
+
+        blenderContext.setBoneContext(boneStructure.getOldMemoryAddress(), this);
+    }
+
+    /**
+     * This method builds the bone. It recursively builds the bone's children.
+     * 
+     * @param bones
+     *            a list of bones where the newly created bone will be added
+     * @param skeletonOwnerOma
+     *            the spatial of the object that will own the skeleton
+     * @param blenderContext
+     *            the blender context
+     * @return newly created bone
+     */
+    public Bone buildBone(List<Bone> bones, Long skeletonOwnerOma, BlenderContext blenderContext) {
+        this.skeletonOwnerOma = skeletonOwnerOma;
+        Long boneOMA = boneStructure.getOldMemoryAddress();
+        bone = new Bone(boneName);
+        bones.add(bone);
+        blenderContext.addLoadedFeatures(boneOMA, boneName, boneStructure, bone);
+        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
+
+        Structure skeletonOwnerObjectStructure = (Structure) blenderContext.getLoadedFeature(skeletonOwnerOma, LoadedFeatureDataType.LOADED_STRUCTURE);
+        Matrix4f invertedObjectOwnerGlobalMatrix = objectHelper.getMatrix(skeletonOwnerObjectStructure, "imat", blenderContext.getBlenderKey().isFixUpAxis());
+        if (objectHelper.isParent(skeletonOwnerOma, armatureObjectOMA)) {
+            boneMatrixInModelSpace = globalBoneMatrix.mult(invertedObjectOwnerGlobalMatrix);
+        } else {
+            boneMatrixInModelSpace = invertedObjectOwnerGlobalMatrix.mult(globalBoneMatrix);
+        }
+
+        Matrix4f boneLocalMatrix = parent == null ? boneMatrixInModelSpace : parent.boneMatrixInModelSpace.invert().multLocal(boneMatrixInModelSpace);
+
+        Vector3f poseLocation = parent == null || !this.is(CONNECTED_TO_PARENT) ? boneLocalMatrix.toTranslationVector() : new Vector3f(0, parent.length, 0);
+        Quaternion rotation = boneLocalMatrix.toRotationQuat().normalizeLocal();
+        Vector3f scale = boneLocalMatrix.toScaleVector();
+
+        bone.setBindTransforms(poseLocation, rotation, scale);
+        for (BoneContext child : children) {
+            bone.addChild(child.buildBone(bones, skeletonOwnerOma, blenderContext));
+        }
+
+        return bone;
+    }
+
+    /**
+     * @return built bone (available after calling 'buildBone' method)
+     */
+    public Bone getBone() {
+        return bone;
+    }
+
+    /**
+     * @return the old memory address of the bone
+     */
+    public Long getBoneOma() {
+        return boneStructure.getOldMemoryAddress();
+    }
+
+    /**
+     * The method returns the length of the bone.
+     * If you want to use it for bone debugger take model space scale into account and do
+     * something like this:
+     * <b>boneContext.getLength() * boneContext.getBone().getModelSpaceScale().y</b>.
+     * Otherwise the bones might not look as they should in the bone debugger.
+     * @return the length of the bone
+     */
+    public float getLength() {
+        return length;
+    }
+
+    /**
+     * @return OMA of the bone's armature object
+     */
+    public Long getArmatureObjectOMA() {
+        return armatureObjectOMA;
+    }
+
+    /**
+     * @return the OMA of the model that owns the bone's skeleton
+     */
+    public Long getSkeletonOwnerOma() {
+        return skeletonOwnerOma;
+    }
+
+    /**
+     * @return the skeleton the bone of this context belongs to
+     */
+    public Skeleton getSkeleton() {
+        return blenderContext.getSkeleton(armatureObjectOMA);
+    }
+
+    /**
+     * Tells if the bone is of specified property defined by its flag.
+     * @param flagMask
+     *            the mask of the flag (constants defined in this class)
+     * @return <b>true</b> if the bone IS of specified proeprty and <b>false</b> otherwise
+     */
+    public boolean is(int flagMask) {
+        return (flag & flagMask) != 0;
+    }
+
+    @Override
+    public String toString() {
+        return "BoneContext: " + boneName;
+    }
+}

+ 243 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java

@@ -0,0 +1,243 @@
+package com.jme3.scene.plugins.blender.animations;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.animation.BoneTrack;
+import com.jme3.animation.SpatialTrack;
+import com.jme3.animation.Track;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.curves.BezierCurve;
+
+/**
+ * This class is used to calculate bezier curves value for the given frames. The
+ * Ipo (interpolation object) consists of several b-spline curves (connected 3rd
+ * degree bezier curves) of a different type.
+ * 
+ * @author Marcin Roguski
+ */
+public class Ipo {
+    private static final Logger LOGGER    = Logger.getLogger(Ipo.class.getName());
+
+    public static final int     AC_LOC_X  = 1;
+    public static final int     AC_LOC_Y  = 2;
+    public static final int     AC_LOC_Z  = 3;
+    public static final int     OB_ROT_X  = 7;
+    public static final int     OB_ROT_Y  = 8;
+    public static final int     OB_ROT_Z  = 9;
+    public static final int     AC_SIZE_X = 13;
+    public static final int     AC_SIZE_Y = 14;
+    public static final int     AC_SIZE_Z = 15;
+    public static final int     AC_QUAT_W = 25;
+    public static final int     AC_QUAT_X = 26;
+    public static final int     AC_QUAT_Y = 27;
+    public static final int     AC_QUAT_Z = 28;
+
+    /** A list of bezier curves for this interpolation object. */
+    private BezierCurve[]       bezierCurves;
+    /** Each ipo contains one bone track. */
+    private Track               calculatedTrack;
+    /** This variable indicates if the Y asxis is the UP axis or not. */
+    protected boolean           fixUpAxis;
+    /**
+     * Depending on the blender version rotations are stored in degrees or
+     * radians so we need to know the version that is used.
+     */
+    protected final int         blenderVersion;
+
+    /**
+     * Constructor. Stores the bezier curves.
+     * 
+     * @param bezierCurves
+     *            a table of bezier curves
+     * @param fixUpAxis
+     *            indicates if the Y is the up axis or not
+     * @param blenderVersion
+     *            the blender version that is currently used
+     */
+    public Ipo(BezierCurve[] bezierCurves, boolean fixUpAxis, int blenderVersion) {
+        this.bezierCurves = bezierCurves;
+        this.fixUpAxis = fixUpAxis;
+        this.blenderVersion = blenderVersion;
+    }
+
+    /**
+     * This method calculates the ipo value for the first curve.
+     * 
+     * @param frame
+     *            the frame for which the value is calculated
+     * @return calculated ipo value
+     */
+    public float calculateValue(int frame) {
+        return this.calculateValue(frame, 0);
+    }
+
+    /**
+     * This method calculates the ipo value for the curve of the specified
+     * index. Make sure you do not exceed the curves amount. Alway chech the
+     * amount of curves before calling this method.
+     * 
+     * @param frame
+     *            the frame for which the value is calculated
+     * @param curveIndex
+     *            the index of the curve
+     * @return calculated ipo value
+     */
+    public float calculateValue(int frame, int curveIndex) {
+        return bezierCurves[curveIndex].evaluate(frame, BezierCurve.Y_VALUE);
+    }
+
+    /**
+     * This method returns the frame where last bezier triple center point of
+     * the specified bezier curve is located.
+     * 
+     * @return the frame number of the last defined bezier triple point for the
+     *         specified ipo
+     */
+    public int getLastFrame() {
+        int result = 1;
+        for (int i = 0; i < bezierCurves.length; ++i) {
+            int tempResult = bezierCurves[i].getLastFrame();
+            if (tempResult > result) {
+                result = tempResult;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * This method calculates the value of the curves as a bone track between
+     * the specified frames.
+     * 
+     * @param targetIndex
+     *            the index of the target for which the method calculates the
+     *            tracks IMPORTANT! Aet to -1 (or any negative number) if you
+     *            want to load spatial animation.
+     * @param localTranslation
+     *            the local translation of the object/bone that will be animated by
+     *            the track
+     * @param localRotation
+     *            the local rotation of the object/bone that will be animated by
+     *            the track
+     * @param localScale
+     *            the local scale of the object/bone that will be animated by
+     *            the track
+     * @param startFrame
+     *            the first frame of tracks (inclusive)
+     * @param stopFrame
+     *            the last frame of the tracks (inclusive)
+     * @param fps
+     *            frame rate (frames per second)
+     * @param spatialTrack
+     *            this flag indicates if the track belongs to a spatial or to a
+     *            bone; the difference is important because it appears that bones
+     *            in blender have the same type of coordinate system (Y as UP)
+     *            as jme while other features have different one (Z is UP)
+     * @return bone track for the specified bone
+     */
+    public Track calculateTrack(int targetIndex, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean spatialTrack) {
+        if (calculatedTrack == null) {
+            // preparing data for track
+            int framesAmount = stopFrame - startFrame;
+            float timeBetweenFrames = 1.0f / fps;
+
+            float[] times = new float[framesAmount + 1];
+            Vector3f[] translations = new Vector3f[framesAmount + 1];
+            float[] translation = new float[] { localTranslation.x, localTranslation.y, localTranslation.z };
+            Quaternion[] rotations = new Quaternion[framesAmount + 1];
+            float[] quaternionRotation = new float[] { localRotation.getX(), localRotation.getY(), localRotation.getZ(), localRotation.getW(), };
+            float[] objectRotation = localRotation.toAngles(null);
+            Vector3f[] scales = new Vector3f[framesAmount + 1];
+            float[] scale = new float[] { localScale.x, localScale.y, localScale.z };
+            float degreeToRadiansFactor = 1;
+            if (blenderVersion < 250) {// in blender earlier than 2.50 the values are stored in degrees
+                degreeToRadiansFactor *= FastMath.DEG_TO_RAD * 10;// the values in blender are divided by 10, so we need to mult it here
+            }
+            int yIndex = 1, zIndex = 2;
+            if (spatialTrack && fixUpAxis) {
+                yIndex = 2;
+                zIndex = 1;
+            }
+
+            // calculating track data
+            for (int frame = startFrame; frame <= stopFrame; ++frame) {
+                int index = frame - startFrame;
+                times[index] = index * timeBetweenFrames;// start + (frame - 1)
+                                                         // * timeBetweenFrames;
+                for (int j = 0; j < bezierCurves.length; ++j) {
+                    double value = bezierCurves[j].evaluate(frame, BezierCurve.Y_VALUE);
+                    switch (bezierCurves[j].getType()) {
+                    // LOCATION
+                        case AC_LOC_X:
+                            translation[0] = (float) value;
+                            break;
+                        case AC_LOC_Y:
+                            if (fixUpAxis && value != 0) {
+                                value = -value;
+                            }
+                            translation[yIndex] = (float) value;
+                            break;
+                        case AC_LOC_Z:
+                            translation[zIndex] = (float) value;
+                            break;
+
+                        // ROTATION (used with object animation)
+                        case OB_ROT_X:
+                            objectRotation[0] = (float) value * degreeToRadiansFactor;
+                            break;
+                        case OB_ROT_Y:
+                            if (fixUpAxis && value != 0) {
+                                value = -value;
+                            }
+                            objectRotation[yIndex] = (float) value * degreeToRadiansFactor;
+                            break;
+                        case OB_ROT_Z:
+                            objectRotation[zIndex] = (float) value * degreeToRadiansFactor;
+                            break;
+
+                        // SIZE
+                        case AC_SIZE_X:
+                            scale[0] = (float) value;
+                            break;
+                        case AC_SIZE_Y:
+                            scale[fixUpAxis ? 2 : 1] = (float) value;
+                            break;
+                        case AC_SIZE_Z:
+                            scale[fixUpAxis ? 1 : 2] = (float) value;
+                            break;
+
+                        // QUATERNION ROTATION (used with bone animation)
+                        case AC_QUAT_W:
+                            quaternionRotation[3] = (float) value;
+                            break;
+                        case AC_QUAT_X:
+                            quaternionRotation[0] = (float) value;
+                            break;
+                        case AC_QUAT_Y:
+                            if (fixUpAxis && value != 0) {
+                                value = -value;
+                            }
+                            quaternionRotation[yIndex] = (float) value;
+                            break;
+                        case AC_QUAT_Z:
+                            quaternionRotation[zIndex] = (float) value;
+                            break;
+                        default:
+                            LOGGER.log(Level.WARNING, "Unknown ipo curve type: {0}.", bezierCurves[j].getType());
+                    }
+                }
+                translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2]));
+                rotations[index] = spatialTrack ? new Quaternion().fromAngles(objectRotation) : new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]);
+                scales[index] = new Vector3f(scale[0], scale[1], scale[2]);
+            }
+            if (spatialTrack) {
+                calculatedTrack = new SpatialTrack(times, translations, rotations, scales);
+            } else {
+                calculatedTrack = new BoneTrack(targetIndex, times, translations, rotations, scales);
+            }
+        }
+        return calculatedTrack;
+    }
+}

+ 194 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/IpoHelper.java

@@ -0,0 +1,194 @@
+package com.jme3.scene.plugins.blender.animations;
+
+import java.util.List;
+import java.util.logging.Logger;
+
+import com.jme3.animation.BoneTrack;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.curves.BezierCurve;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.BlenderInputStream;
+import com.jme3.scene.plugins.blender.file.FileBlockHeader;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class helps to compute values from interpolation curves for features
+ * like animation or constraint influence. The curves are 3rd degree bezier
+ * curves.
+ * 
+ * @author Marcin Roguski
+ */
+public class IpoHelper extends AbstractBlenderHelper {
+    private static final Logger LOGGER = Logger.getLogger(IpoHelper.class.getName());
+
+    /**
+     * This constructor parses the given blender version and stores the result.
+     * Some functionalities may differ in different blender versions.
+     * 
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public IpoHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    /**
+     * This method creates an ipo object used for interpolation calculations.
+     * 
+     * @param ipoStructure
+     *            the structure with ipo definition
+     * @param blenderContext
+     *            the blender context
+     * @return the ipo object
+     * @throws BlenderFileException
+     *             this exception is thrown when the blender file is somehow
+     *             corrupted
+     */
+    public Ipo fromIpoStructure(Structure ipoStructure, BlenderContext blenderContext) throws BlenderFileException {
+        Structure curvebase = (Structure) ipoStructure.getFieldValue("curve");
+
+        // preparing bezier curves
+        Ipo result = null;
+        List<Structure> curves = curvebase.evaluateListBase();// IpoCurve
+        if (curves.size() > 0) {
+            BezierCurve[] bezierCurves = new BezierCurve[curves.size()];
+            int frame = 0;
+            for (Structure curve : curves) {
+                Pointer pBezTriple = (Pointer) curve.getFieldValue("bezt");
+                List<Structure> bezTriples = pBezTriple.fetchData();
+                int type = ((Number) curve.getFieldValue("adrcode")).intValue();
+                bezierCurves[frame++] = new BezierCurve(type, bezTriples, 2);
+            }
+            curves.clear();
+            result = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
+            blenderContext.addLoadedFeatures(ipoStructure.getOldMemoryAddress(), ipoStructure.getName(), ipoStructure, result);
+        }
+        return result;
+    }
+
+    /**
+     * This method creates an ipo object used for interpolation calculations. It
+     * should be called for blender version 2.50 and higher.
+     * 
+     * @param actionStructure
+     *            the structure with action definition
+     * @param blenderContext
+     *            the blender context
+     * @return the ipo object
+     * @throws BlenderFileException
+     *             this exception is thrown when the blender file is somehow
+     *             corrupted
+     */
+    public Ipo fromAction(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException {
+        Ipo result = null;
+        List<Structure> curves = ((Structure) actionStructure.getFieldValue("curves")).evaluateListBase();// FCurve
+        if (curves.size() > 0) {
+            BezierCurve[] bezierCurves = new BezierCurve[curves.size()];
+            int frame = 0;
+            for (Structure curve : curves) {
+                Pointer pBezTriple = (Pointer) curve.getFieldValue("bezt");
+                List<Structure> bezTriples = pBezTriple.fetchData();
+                int type = this.getCurveType(curve, blenderContext);
+                bezierCurves[frame++] = new BezierCurve(type, bezTriples, 2);
+            }
+            curves.clear();
+            result = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
+        }
+        return result;
+    }
+
+    /**
+     * This method returns the type of the ipo curve.
+     * 
+     * @param structure
+     *            the structure must contain the 'rna_path' field and
+     *            'array_index' field (the type is not important here)
+     * @param blenderContext
+     *            the blender context
+     * @return the type of the curve
+     */
+    public int getCurveType(Structure structure, BlenderContext blenderContext) {
+        // reading rna path first
+        BlenderInputStream bis = blenderContext.getInputStream();
+        int currentPosition = bis.getPosition();
+        Pointer pRnaPath = (Pointer) structure.getFieldValue("rna_path");
+        FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pRnaPath.getOldMemoryAddress());
+        bis.setPosition(dataFileBlock.getBlockPosition());
+        String rnaPath = bis.readString();
+        bis.setPosition(currentPosition);
+        int arrayIndex = ((Number) structure.getFieldValue("array_index")).intValue();
+
+        // determining the curve type
+        if (rnaPath.endsWith("location")) {
+            return Ipo.AC_LOC_X + arrayIndex;
+        }
+        if (rnaPath.endsWith("rotation_quaternion")) {
+            return Ipo.AC_QUAT_W + arrayIndex;
+        }
+        if (rnaPath.endsWith("scale")) {
+            return Ipo.AC_SIZE_X + arrayIndex;
+        }
+        if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) {
+            return Ipo.OB_ROT_X + arrayIndex;
+        }
+        LOGGER.warning("Unknown curve rna path: " + rnaPath);
+        return -1;
+    }
+
+    /**
+     * This method creates an ipo with only a single value. No track type is
+     * specified so do not use it for calculating tracks.
+     * 
+     * @param constValue
+     *            the value of this ipo
+     * @return constant ipo
+     */
+    public Ipo fromValue(float constValue) {
+        return new ConstIpo(constValue);
+    }
+
+    /**
+     * Ipo constant curve. This is a curve with only one value and no specified
+     * type. This type of ipo cannot be used to calculate tracks. It should only
+     * be used to calculate single value for a given frame.
+     * 
+     * @author Marcin Roguski (Kaelthas)
+     */
+    private class ConstIpo extends Ipo {
+
+        /** The constant value of this ipo. */
+        private float constValue;
+
+        /**
+         * Constructor. Stores the constant value of this ipo.
+         * 
+         * @param constValue
+         *            the constant value of this ipo
+         */
+        public ConstIpo(float constValue) {
+            super(null, false, 0);// the version is not important here
+            this.constValue = constValue;
+        }
+
+        @Override
+        public float calculateValue(int frame) {
+            return constValue;
+        }
+
+        @Override
+        public float calculateValue(int frame, int curveIndex) {
+            return constValue;
+        }
+
+        @Override
+        public BoneTrack calculateTrack(int boneIndex, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean boneTrack) {
+            throw new IllegalStateException("Constatnt ipo object cannot be used for calculating bone tracks!");
+        }
+    }
+}

+ 147 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/cameras/CameraHelper.java

@@ -0,0 +1,147 @@
+package com.jme3.scene.plugins.blender.cameras;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.math.FastMath;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.CameraNode;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * A class that is used to load cameras into the scene.
+ * @author Marcin Roguski
+ */
+public class CameraHelper extends AbstractBlenderHelper {
+
+    private static final Logger LOGGER             = Logger.getLogger(CameraHelper.class.getName());
+    protected static final int  DEFAULT_CAM_WIDTH  = 640;
+    protected static final int  DEFAULT_CAM_HEIGHT = 480;
+
+    /**
+     * This constructor parses the given blender version and stores the result. Some functionalities may differ in
+     * different blender versions.
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public CameraHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    /**
+     * This method converts the given structure to jme camera.
+     * 
+     * @param structure
+     *            camera structure
+     * @return jme camera object
+     * @throws BlenderFileException
+     *             an exception is thrown when there are problems with the
+     *             blender file
+     */
+    public CameraNode toCamera(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
+        if (blenderVersion >= 250) {
+            return this.toCamera250(structure, blenderContext.getSceneStructure());
+        } else {
+            return this.toCamera249(structure);
+        }
+    }
+
+    /**
+     * This method converts the given structure to jme camera. Should be used form blender 2.5+.
+     * 
+     * @param structure
+     *            camera structure
+     * @param sceneStructure
+     *            scene structure
+     * @return jme camera object
+     * @throws BlenderFileException
+     *             an exception is thrown when there are problems with the
+     *             blender file
+     */
+    private CameraNode toCamera250(Structure structure, Structure sceneStructure) throws BlenderFileException {
+        int width = DEFAULT_CAM_WIDTH;
+        int height = DEFAULT_CAM_HEIGHT;
+        if (sceneStructure != null) {
+            Structure renderData = (Structure) sceneStructure.getFieldValue("r");
+            width = ((Number) renderData.getFieldValue("xsch")).shortValue();
+            height = ((Number) renderData.getFieldValue("ysch")).shortValue();
+        }
+        Camera camera = new Camera(width, height);
+        int type = ((Number) structure.getFieldValue("type")).intValue();
+        if (type != 0 && type != 1) {
+            LOGGER.log(Level.WARNING, "Unknown camera type: {0}. Perspective camera is being used!", type);
+            type = 0;
+        }
+        // type==0 - perspective; type==1 - orthographic; perspective is used as default
+        camera.setParallelProjection(type == 1);
+        float aspect = width / (float) height;
+        float fovY; // Vertical field of view in degrees
+        float clipsta = ((Number) structure.getFieldValue("clipsta")).floatValue();
+        float clipend = ((Number) structure.getFieldValue("clipend")).floatValue();
+        if (type == 0) {
+            // Convert lens MM to vertical degrees in fovY, see Blender rna_Camera_angle_get()
+            // Default sensor size prior to 2.60 was 32.
+            float sensor = 32.0f;
+            boolean sensorVertical = false;
+            Number sensorFit = (Number) structure.getFieldValue("sensor_fit");
+            if (sensorFit != null) {
+                // If sensor_fit is vert (2), then sensor_y is used
+                sensorVertical = sensorFit.byteValue() == 2;
+                String sensorName = "sensor_x";
+                if (sensorVertical) {
+                    sensorName = "sensor_y";
+                }
+                sensor = ((Number) structure.getFieldValue(sensorName)).floatValue();
+            }
+            float focalLength = ((Number) structure.getFieldValue("lens")).floatValue();
+            float fov = 2.0f * FastMath.atan((sensor / 2.0f) / focalLength);
+            if (sensorVertical) {
+                fovY = fov * FastMath.RAD_TO_DEG;
+            } else {
+                // Convert fov from horizontal to vertical
+                fovY = 2.0f * FastMath.atan(FastMath.tan(fov / 2.0f) / aspect) * FastMath.RAD_TO_DEG;
+            }
+        } else {
+            // This probably is not correct.
+            fovY = ((Number) structure.getFieldValue("ortho_scale")).floatValue();
+        }
+        camera.setFrustumPerspective(fovY, aspect, clipsta, clipend);
+        return new CameraNode(null, camera);
+    }
+
+    /**
+     * This method converts the given structure to jme camera. Should be used form blender 2.49.
+     * 
+     * @param structure
+     *            camera structure
+     * @return jme camera object
+     * @throws BlenderFileException
+     *             an exception is thrown when there are problems with the
+     *             blender file
+     */
+    private CameraNode toCamera249(Structure structure) throws BlenderFileException {
+        Camera camera = new Camera(DEFAULT_CAM_WIDTH, DEFAULT_CAM_HEIGHT);
+        int type = ((Number) structure.getFieldValue("type")).intValue();
+        if (type != 0 && type != 1) {
+            LOGGER.log(Level.WARNING, "Unknown camera type: {0}. Perspective camera is being used!", type);
+            type = 0;
+        }
+        // type==0 - perspective; type==1 - orthographic; perspective is used as default
+        camera.setParallelProjection(type == 1);
+        float aspect = 0;
+        float clipsta = ((Number) structure.getFieldValue("clipsta")).floatValue();
+        float clipend = ((Number) structure.getFieldValue("clipend")).floatValue();
+        if (type == 0) {
+            aspect = ((Number) structure.getFieldValue("lens")).floatValue();
+        } else {
+            aspect = ((Number) structure.getFieldValue("ortho_scale")).floatValue();
+        }
+        camera.setFrustumPerspective(aspect, camera.getWidth() / camera.getHeight(), clipsta, clipend);
+        return new CameraNode(null, camera);
+    }
+}

+ 73 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/BoneConstraint.java

@@ -0,0 +1,73 @@
+package com.jme3.scene.plugins.blender.constraints;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.animations.Ipo;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * Constraint applied on the bone.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class BoneConstraint extends Constraint {
+    private static final Logger LOGGER = Logger.getLogger(BoneConstraint.class.getName());
+
+    /**
+     * The bone constraint constructor.
+     * 
+     * @param constraintStructure
+     *            the constraint's structure
+     * @param ownerOMA
+     *            the OMA of the bone that owns the constraint
+     * @param influenceIpo
+     *            the influence interpolation curve
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             exception thrown when problems with blender file occur
+     */
+    public BoneConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
+        super(constraintStructure, ownerOMA, influenceIpo, blenderContext);
+    }
+
+    @Override
+    public boolean validate() {
+        if (targetOMA != null) {
+            Spatial nodeTarget = (Spatial) blenderContext.getLoadedFeature(targetOMA, LoadedFeatureDataType.LOADED_FEATURE);
+            if (nodeTarget == null) {
+                LOGGER.log(Level.WARNING, "Cannot find target for constraint: {0}.", name);
+                return false;
+            }
+            // the second part of the if expression verifies if the found node
+            // (if any) is an armature node
+            if (blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, nodeTarget) != null) {
+                if (subtargetName.trim().isEmpty()) {
+                    LOGGER.log(Level.WARNING, "No bone target specified for constraint: {0}.", name);
+                    return false;
+                }
+                // if the target is not an object node then it is an Armature,
+                // so make sure the bone is in the current skeleton
+                BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
+                if (targetOMA.longValue() != boneContext.getArmatureObjectOMA().longValue()) {
+                    LOGGER.log(Level.WARNING, "Bone constraint {0} must target bone in the its own skeleton! Targeting bone in another skeleton is not supported!", name);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+    
+    @Override
+    public void apply(int frame) {
+        super.apply(frame);
+        blenderContext.getBoneContext(ownerOMA).getBone().updateWorldVectors();
+    }
+}

+ 166 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java

@@ -0,0 +1,166 @@
+package com.jme3.scene.plugins.blender.constraints;
+
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.math.Transform;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.animations.Ipo;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinition;
+import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinitionFactory;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * The implementation of a constraint.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public abstract class Constraint {
+    private static final Logger          LOGGER = Logger.getLogger(Constraint.class.getName());
+
+    /** The name of this constraint. */
+    protected final String               name;
+    /** Indicates if the constraint is already baked or not. */
+    protected boolean                    baked;
+
+    protected Space                      ownerSpace;
+    protected final ConstraintDefinition constraintDefinition;
+    protected Long                       ownerOMA;
+
+    protected Long                       targetOMA;
+    protected Space                      targetSpace;
+    protected String                     subtargetName;
+
+    /** The ipo object defining influence. */
+    protected final Ipo                  ipo;
+    /** The blender context. */
+    protected final BlenderContext       blenderContext;
+    protected final ConstraintHelper     constraintHelper;
+
+    /**
+     * This constructor creates the constraint instance.
+     * 
+     * @param constraintStructure
+     *            the constraint's structure (bConstraint clss in blender 2.49).
+     * @param ownerOMA
+     *            the old memory address of the constraint owner
+     * @param ownerType
+     *            the type of the constraint owner
+     * @param influenceIpo
+     *            the ipo curve of the influence factor
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             this exception is thrown when the blender file is somehow
+     *             corrupted
+     */
+    public Constraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
+        this.blenderContext = blenderContext;
+        name = constraintStructure.getFieldValue("name").toString();
+        Pointer pData = (Pointer) constraintStructure.getFieldValue("data");
+        if (pData.isNotNull()) {
+            Structure data = pData.fetchData().get(0);
+            constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(data, ownerOMA, blenderContext);
+            Pointer pTar = (Pointer) data.getFieldValue("tar");
+            if (pTar != null && pTar.isNotNull()) {
+                targetOMA = pTar.getOldMemoryAddress();
+                targetSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("tarspace")).byteValue());
+                Object subtargetValue = data.getFieldValue("subtarget");
+                if (subtargetValue != null) {// not all constraint data have the
+                                             // subtarget field
+                    subtargetName = subtargetValue.toString();
+                }
+            }
+        } else {
+            // Null constraint has no data, so create it here
+            constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(null, null, blenderContext);
+        }
+        ownerSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("ownspace")).byteValue());
+        ipo = influenceIpo;
+        this.ownerOMA = ownerOMA;
+        constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
+        LOGGER.log(Level.INFO, "Created constraint: {0} with definition: {1}", new Object[] { name, constraintDefinition });
+    }
+
+    /**
+     * @return <b>true</b> if the constraint is implemented and <b>false</b>
+     *         otherwise
+     */
+    public boolean isImplemented() {
+        return constraintDefinition == null ? true : constraintDefinition.isImplemented();
+    }
+
+    /**
+     * @return the name of the constraint type, similar to the constraint name
+     *         used in Blender
+     */
+    public String getConstraintTypeName() {
+        return constraintDefinition.getConstraintTypeName();
+    }
+
+    /**
+     * @return the OMAs of the features whose transform had been altered beside the constraint owner
+     */
+    public Set<Long> getAlteredOmas() {
+        return constraintDefinition.getAlteredOmas();
+    }
+
+    /**
+     * Performs validation before baking. Checks factors that can prevent
+     * constraint from baking that could not be checked during constraint
+     * loading.
+     */
+    public abstract boolean validate();
+
+    /**
+     * Applies the constraint to owner (and in some cases can alter other bones of the skeleton).
+     * @param frame
+     *            the frame of the animation
+     */
+    public void apply(int frame) {
+        Transform targetTransform = targetOMA != null ? constraintHelper.getTransform(targetOMA, subtargetName, targetSpace) : null;
+        constraintDefinition.bake(ownerSpace, targetSpace, targetTransform, ipo.calculateValue(frame));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (name == null ? 0 : name.hashCode());
+        result = prime * result + (ownerOMA == null ? 0 : ownerOMA.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (this.getClass() != obj.getClass()) {
+            return false;
+        }
+        Constraint other = (Constraint) obj;
+        if (name == null) {
+            if (other.name != null) {
+                return false;
+            }
+        } else if (!name.equals(other.name)) {
+            return false;
+        }
+        if (ownerOMA == null) {
+            if (other.ownerOMA != null) {
+                return false;
+            }
+        } else if (!ownerOMA.equals(other.ownerOMA)) {
+            return false;
+        }
+        return true;
+    }
+}

+ 478 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/ConstraintHelper.java

@@ -0,0 +1,478 @@
+package com.jme3.scene.plugins.blender.constraints;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+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.Spatial;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.animations.Ipo;
+import com.jme3.scene.plugins.blender.animations.IpoHelper;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.objects.ObjectHelper;
+import com.jme3.util.TempVars;
+
+/**
+ * This class should be used for constraint calculations.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class ConstraintHelper extends AbstractBlenderHelper {
+    private static final Logger     LOGGER                      = Logger.getLogger(ConstraintHelper.class.getName());
+
+    private static final Quaternion POS_PARLOC_SPACE_QUATERNION = new Quaternion(new float[] { FastMath.HALF_PI, 0, 0 });
+    private static final Quaternion NEG_PARLOC_SPACE_QUATERNION = new Quaternion(new float[] { -FastMath.HALF_PI, 0, 0 });
+
+    /**
+     * Helper constructor.
+     * 
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public ConstraintHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    /**
+     * This method reads constraints for for the given structure. The
+     * constraints are loaded only once for object/bone.
+     * 
+     * @param objectStructure
+     *            the structure we read constraint's for
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     */
+    public void loadConstraints(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException {
+        LOGGER.fine("Loading constraints.");
+        // reading influence ipos for the constraints
+        IpoHelper ipoHelper = blenderContext.getHelper(IpoHelper.class);
+        Map<String, Map<String, Ipo>> constraintsIpos = new HashMap<String, Map<String, Ipo>>();
+        Pointer pActions = (Pointer) objectStructure.getFieldValue("action");
+        if (pActions.isNotNull()) {
+            List<Structure> actions = pActions.fetchData();
+            for (Structure action : actions) {
+                Structure chanbase = (Structure) action.getFieldValue("chanbase");
+                List<Structure> actionChannels = chanbase.evaluateListBase();
+                for (Structure actionChannel : actionChannels) {
+                    Map<String, Ipo> ipos = new HashMap<String, Ipo>();
+                    Structure constChannels = (Structure) actionChannel.getFieldValue("constraintChannels");
+                    List<Structure> constraintChannels = constChannels.evaluateListBase();
+                    for (Structure constraintChannel : constraintChannels) {
+                        Pointer pIpo = (Pointer) constraintChannel.getFieldValue("ipo");
+                        if (pIpo.isNotNull()) {
+                            String constraintName = constraintChannel.getFieldValue("name").toString();
+                            Ipo ipo = ipoHelper.fromIpoStructure(pIpo.fetchData().get(0), blenderContext);
+                            ipos.put(constraintName, ipo);
+                        }
+                    }
+                    String actionName = actionChannel.getFieldValue("name").toString();
+                    constraintsIpos.put(actionName, ipos);
+                }
+            }
+        }
+
+        // loading constraints connected with the object's bones
+        Pointer pPose = (Pointer) objectStructure.getFieldValue("pose");
+        if (pPose.isNotNull()) {
+            List<Structure> poseChannels = ((Structure) pPose.fetchData().get(0).getFieldValue("chanbase")).evaluateListBase();
+            for (Structure poseChannel : poseChannels) {
+                List<Constraint> constraintsList = new ArrayList<Constraint>();
+                Long boneOMA = Long.valueOf(((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress());
+
+                // the name is read directly from structure because bone might
+                // not yet be loaded
+                String name = blenderContext.getFileBlock(boneOMA).getStructure(blenderContext).getFieldValue("name").toString();
+                List<Structure> constraints = ((Structure) poseChannel.getFieldValue("constraints")).evaluateListBase();
+                for (Structure constraint : constraints) {
+                    String constraintName = constraint.getFieldValue("name").toString();
+                    Map<String, Ipo> ipoMap = constraintsIpos.get(name);
+                    Ipo ipo = ipoMap == null ? null : ipoMap.get(constraintName);
+                    if (ipo == null) {
+                        float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue();
+                        ipo = ipoHelper.fromValue(enforce);
+                    }
+                    constraintsList.add(new BoneConstraint(constraint, boneOMA, ipo, blenderContext));
+                }
+                blenderContext.addConstraints(boneOMA, constraintsList);
+            }
+        }
+
+        // loading constraints connected with the object itself
+        List<Structure> constraints = ((Structure) objectStructure.getFieldValue("constraints")).evaluateListBase();
+        if (constraints != null && constraints.size() > 0) {
+            Pointer pData = (Pointer) objectStructure.getFieldValue("data");
+            String dataType = pData.isNotNull() ? pData.fetchData().get(0).getType() : null;
+            List<Constraint> constraintsList = new ArrayList<Constraint>(constraints.size());
+
+            for (Structure constraint : constraints) {
+                String constraintName = constraint.getFieldValue("name").toString();
+                String objectName = objectStructure.getName();
+
+                Map<String, Ipo> objectConstraintsIpos = constraintsIpos.get(objectName);
+                Ipo ipo = objectConstraintsIpos != null ? objectConstraintsIpos.get(constraintName) : null;
+                if (ipo == null) {
+                    float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue();
+                    ipo = ipoHelper.fromValue(enforce);
+                }
+
+                constraintsList.add(this.createConstraint(dataType, constraint, objectStructure.getOldMemoryAddress(), ipo, blenderContext));
+            }
+            blenderContext.addConstraints(objectStructure.getOldMemoryAddress(), constraintsList);
+        }
+    }
+
+    /**
+     * This method creates a proper constraint object depending on the object's
+     * data type. Supported data types: <li>Mesh <li>Armature <li>Camera <li>
+     * Lamp Bone constraints are created in a different place.
+     * 
+     * @param dataType
+     *            the type of the object's data
+     * @param constraintStructure
+     *            the constraint structure
+     * @param ownerOMA
+     *            the owner OMA
+     * @param influenceIpo
+     *            the influence interpolation curve
+     * @param blenderContext
+     *            the blender context
+     * @return constraint object for the required type
+     * @throws BlenderFileException
+     *             thrown when problems with blender file occured
+     */
+    private Constraint createConstraint(String dataType, Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
+        if (dataType == null || "Mesh".equalsIgnoreCase(dataType) || "Camera".equalsIgnoreCase(dataType) || "Lamp".equalsIgnoreCase(dataType)) {
+            return new SpatialConstraint(constraintStructure, ownerOMA, influenceIpo, blenderContext);
+        } else if ("Armature".equalsIgnoreCase(dataType)) {
+            return new SkeletonConstraint(constraintStructure, ownerOMA, influenceIpo, blenderContext);
+        } else {
+            throw new IllegalArgumentException("Unsupported data type for applying constraints: " + dataType);
+        }
+    }
+
+    /**
+     * The method bakes all available and valid constraints.
+     * 
+     * @param blenderContext
+     *            the blender context
+     */
+    public void bakeConstraints(BlenderContext blenderContext) {
+        List<SimulationNode> simulationRootNodes = new ArrayList<SimulationNode>();
+        for (Constraint constraint : blenderContext.getAllConstraints()) {
+            boolean constraintUsed = false;
+            for (SimulationNode node : simulationRootNodes) {
+                if (node.contains(constraint)) {
+                    constraintUsed = true;
+                    break;
+                }
+            }
+
+            if (!constraintUsed) {
+                if (constraint instanceof BoneConstraint) {
+                    BoneContext boneContext = blenderContext.getBoneContext(constraint.ownerOMA);
+                    simulationRootNodes.add(new SimulationNode(boneContext.getArmatureObjectOMA(), blenderContext));
+                } else if (constraint instanceof SpatialConstraint) {
+                    Spatial spatial = (Spatial) blenderContext.getLoadedFeature(constraint.ownerOMA, LoadedFeatureDataType.LOADED_FEATURE);
+                    while (spatial.getParent() != null) {
+                        spatial = spatial.getParent();
+                    }
+                    simulationRootNodes.add(new SimulationNode((Long) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, spatial), blenderContext));
+                } else {
+                    throw new IllegalStateException("Unsupported constraint type: " + constraint);
+                }
+            }
+        }
+
+        for (SimulationNode node : simulationRootNodes) {
+            node.simulate();
+        }
+    }
+
+    /**
+     * The method retreives the transform from a feature in a given space.
+     * 
+     * @param oma
+     *            the OMA of the feature (spatial or armature node)
+     * @param subtargetName
+     *            the feature's subtarget (bone in a case of armature's node)
+     * @param space
+     *            the space the transform is evaluated to
+     * @return thensform of a feature in a given space
+     */
+    public Transform getTransform(Long oma, String subtargetName, Space space) {
+        Spatial feature = (Spatial) blenderContext.getLoadedFeature(oma, LoadedFeatureDataType.LOADED_FEATURE);
+        boolean isArmature = blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, feature) != null;
+        if (isArmature) {
+            blenderContext.getSkeleton(oma).updateWorldVectors();
+            BoneContext targetBoneContext = blenderContext.getBoneByName(oma, subtargetName);
+            Bone bone = targetBoneContext.getBone();
+
+            if (bone.getParent() == null && (space == Space.CONSTRAINT_SPACE_LOCAL || space == Space.CONSTRAINT_SPACE_PARLOCAL)) {
+                space = Space.CONSTRAINT_SPACE_POSE;
+            }
+
+            TempVars tempVars = TempVars.get();// use readable names of the matrices so that the code is more clear
+            Transform result;
+            switch (space) {
+                case CONSTRAINT_SPACE_WORLD:
+                    Spatial model = (Spatial) blenderContext.getLoadedFeature(targetBoneContext.getSkeletonOwnerOma(), LoadedFeatureDataType.LOADED_FEATURE);
+                    Matrix4f boneModelMatrix = this.toMatrix(bone.getModelSpacePosition(), bone.getModelSpaceRotation(), bone.getModelSpaceScale(), tempVars.tempMat4);
+                    Matrix4f modelWorldMatrix = this.toMatrix(model.getWorldTransform(), tempVars.tempMat42);
+                    Matrix4f boneMatrixInWorldSpace = modelWorldMatrix.multLocal(boneModelMatrix);
+                    result = new Transform(boneMatrixInWorldSpace.toTranslationVector(), boneMatrixInWorldSpace.toRotationQuat(), boneMatrixInWorldSpace.toScaleVector());
+                    break;
+                case CONSTRAINT_SPACE_LOCAL:
+                    assert bone.getParent() != null : "CONSTRAINT_SPACE_LOCAL should be evaluated as CONSTRAINT_SPACE_POSE if the bone has no parent!";
+                    result = new Transform(bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale());
+                    break;
+                case CONSTRAINT_SPACE_POSE:
+                    Matrix4f boneWorldMatrix = this.toMatrix(this.getTransform(oma, subtargetName, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat4);
+                    Matrix4f armatureInvertedWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat42).invertLocal();
+                    Matrix4f bonePoseMatrix = armatureInvertedWorldMatrix.multLocal(boneWorldMatrix);
+                    result = new Transform(bonePoseMatrix.toTranslationVector(), bonePoseMatrix.toRotationQuat(), bonePoseMatrix.toScaleVector());
+                    break;
+                case CONSTRAINT_SPACE_PARLOCAL:
+                    Matrix4f parentLocalMatrix = tempVars.tempMat4;
+                    if (bone.getParent() != null) {
+                        Bone parent = bone.getParent();
+                        this.toMatrix(parent.getLocalPosition(), parent.getLocalRotation(), parent.getLocalScale(), parentLocalMatrix);
+                    } else {
+                        parentLocalMatrix.loadIdentity();
+                    }
+                    Matrix4f boneLocalMatrix = this.toMatrix(bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale(), tempVars.tempMat42);
+                    Matrix4f resultMatrix = parentLocalMatrix.multLocal(boneLocalMatrix);
+
+                    Vector3f loc = resultMatrix.toTranslationVector();
+                    Quaternion rot = resultMatrix.toRotationQuat().normalizeLocal().multLocal(NEG_PARLOC_SPACE_QUATERNION);
+                    Vector3f scl = resultMatrix.toScaleVector();
+                    result = new Transform(loc, rot, scl);
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown space type: " + space);
+            }
+            tempVars.release();
+            return result;
+        } else {
+            switch (space) {
+                case CONSTRAINT_SPACE_LOCAL:
+                    return feature.getLocalTransform();
+                case CONSTRAINT_SPACE_WORLD:
+                    return feature.getWorldTransform();
+                case CONSTRAINT_SPACE_PARLOCAL:
+                case CONSTRAINT_SPACE_POSE:
+                    throw new IllegalStateException("Nodes can have only Local and World spaces applied!");
+                default:
+                    throw new IllegalStateException("Unknown space type: " + space);
+            }
+        }
+    }
+
+    /**
+     * Applies transform to a feature (bone or spatial). Computations transform
+     * the given transformation from the given space to the feature's local
+     * space.
+     * 
+     * @param oma
+     *            the OMA of the feature we apply transformation to
+     * @param subtargetName
+     *            the name of the feature's subtarget (bone in case of armature)
+     * @param space
+     *            the space in which the given transform is to be applied
+     * @param transform
+     *            the transform we apply
+     */
+    public void applyTransform(Long oma, String subtargetName, Space space, Transform transform) {
+        Spatial feature = (Spatial) blenderContext.getLoadedFeature(oma, LoadedFeatureDataType.LOADED_FEATURE);
+        boolean isArmature = blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, feature) != null;
+        if (isArmature) {
+            Skeleton skeleton = blenderContext.getSkeleton(oma);
+            BoneContext targetBoneContext = blenderContext.getBoneByName(oma, subtargetName);
+            Bone bone = targetBoneContext.getBone();
+
+            if (bone.getParent() == null && (space == Space.CONSTRAINT_SPACE_LOCAL || space == Space.CONSTRAINT_SPACE_PARLOCAL)) {
+                space = Space.CONSTRAINT_SPACE_POSE;
+            }
+
+            TempVars tempVars = TempVars.get();
+            switch (space) {
+                case CONSTRAINT_SPACE_LOCAL:
+                    assert bone.getParent() != null : "CONSTRAINT_SPACE_LOCAL should be evaluated as CONSTRAINT_SPACE_POSE if the bone has no parent!";
+                    bone.setBindTransforms(transform.getTranslation(), transform.getRotation(), transform.getScale());
+                    break;
+                case CONSTRAINT_SPACE_WORLD: {
+                    Matrix4f boneMatrixInWorldSpace = this.toMatrix(transform, tempVars.tempMat4);
+                    Matrix4f modelWorldMatrix = this.toMatrix(this.getTransform(targetBoneContext.getSkeletonOwnerOma(), null, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat42);
+                    Matrix4f boneMatrixInModelSpace = modelWorldMatrix.invertLocal().multLocal(boneMatrixInWorldSpace);
+                    Bone parent = bone.getParent();
+                    if (parent != null) {
+                        Matrix4f parentMatrixInModelSpace = this.toMatrix(parent.getModelSpacePosition(), parent.getModelSpaceRotation(), parent.getModelSpaceScale(), tempVars.tempMat4);
+                        boneMatrixInModelSpace = parentMatrixInModelSpace.invertLocal().multLocal(boneMatrixInModelSpace);
+                    }
+                    bone.setBindTransforms(boneMatrixInModelSpace.toTranslationVector(), boneMatrixInModelSpace.toRotationQuat(), boneMatrixInModelSpace.toScaleVector());
+                    break;
+                }
+                case CONSTRAINT_SPACE_POSE: {
+                    Matrix4f armatureWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat4);
+                    Matrix4f boneMatrixInWorldSpace = armatureWorldMatrix.multLocal(this.toMatrix(transform, tempVars.tempMat42));
+                    Matrix4f invertedModelMatrix = this.toMatrix(this.getTransform(targetBoneContext.getSkeletonOwnerOma(), null, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat42).invertLocal();
+                    Matrix4f boneMatrixInModelSpace = invertedModelMatrix.multLocal(boneMatrixInWorldSpace);
+                    Bone parent = bone.getParent();
+                    if (parent != null) {
+                        Matrix4f parentMatrixInModelSpace = this.toMatrix(parent.getModelSpacePosition(), parent.getModelSpaceRotation(), parent.getModelSpaceScale(), tempVars.tempMat4);
+                        boneMatrixInModelSpace = parentMatrixInModelSpace.invertLocal().multLocal(boneMatrixInModelSpace);
+                    }
+                    bone.setBindTransforms(boneMatrixInModelSpace.toTranslationVector(), boneMatrixInModelSpace.toRotationQuat(), boneMatrixInModelSpace.toScaleVector());
+                    break;
+                }
+                case CONSTRAINT_SPACE_PARLOCAL:
+                    Matrix4f parentLocalInverseMatrix = tempVars.tempMat4;
+                    if (bone.getParent() != null) {
+                        this.toMatrix(bone.getParent().getLocalPosition(), bone.getParent().getLocalRotation(), bone.getParent().getLocalScale(), parentLocalInverseMatrix);
+                        parentLocalInverseMatrix.invertLocal();
+                    } else {
+                        parentLocalInverseMatrix.loadIdentity();
+                    }
+                    Matrix4f m = this.toMatrix(transform.getTranslation(), transform.getRotation(), transform.getScale(), tempVars.tempMat42);
+                    Matrix4f result = parentLocalInverseMatrix.multLocal(m);
+                    Vector3f loc = result.toTranslationVector();
+                    Quaternion rot = result.toRotationQuat().normalizeLocal().multLocal(POS_PARLOC_SPACE_QUATERNION);
+                    Vector3f scl = result.toScaleVector();
+                    bone.setBindTransforms(loc, rot, scl);
+                    break;
+                default:
+                    tempVars.release();
+                    throw new IllegalStateException("Invalid space type for target object: " + space.toString());
+            }
+            tempVars.release();
+            skeleton.updateWorldVectors();
+        } else {
+            switch (space) {
+                case CONSTRAINT_SPACE_LOCAL:
+                    feature.getLocalTransform().set(transform);
+                    break;
+                case CONSTRAINT_SPACE_WORLD:
+                    if (feature.getParent() == null) {
+                        feature.setLocalTransform(transform);
+                    } else {
+                        Transform parentWorldTransform = feature.getParent().getWorldTransform();
+
+                        TempVars tempVars = TempVars.get();
+                        Matrix4f parentInverseMatrix = this.toMatrix(parentWorldTransform, tempVars.tempMat4).invertLocal();
+                        Matrix4f m = this.toMatrix(transform, tempVars.tempMat42);
+                        m = m.multLocal(parentInverseMatrix);
+                        tempVars.release();
+
+                        transform.setTranslation(m.toTranslationVector());
+                        transform.setRotation(m.toRotationQuat());
+                        transform.setScale(m.toScaleVector());
+
+                        feature.setLocalTransform(transform);
+                    }
+                    break;
+                default:
+                    throw new IllegalStateException("Invalid space type for spatial object: " + space.toString());
+            }
+        }
+    }
+
+    /**
+     * Converts given transform to the matrix.
+     * 
+     * @param transform
+     *            the transform to be converted
+     * @param store
+     *            the matrix where the result will be stored
+     * @return the store matrix
+     */
+    public Matrix4f toMatrix(Transform transform, Matrix4f store) {
+        if (transform != null) {
+            return this.toMatrix(transform.getTranslation(), transform.getRotation(), transform.getScale(), store);
+        }
+        store.loadIdentity();
+        return store;
+    }
+
+    /**
+     * Converts given transformation parameters into the matrix.
+     * 
+     * @param position
+     *            the position of the feature
+     * @param rotation
+     *            the rotation of the feature
+     * @param scale
+     *            the scale of the feature
+     * @param store
+     *            the matrix where the result will be stored
+     * @return the store matrix
+     */
+    private Matrix4f toMatrix(Vector3f position, Quaternion rotation, Vector3f scale, Matrix4f store) {
+        store.loadIdentity();
+        store.setTranslation(position);
+        store.setRotationQuaternion(rotation);
+        store.setScale(scale);
+        return store;
+    }
+
+    /**
+     * The space of target or owner transformation.
+     * 
+     * @author Marcin Roguski (Kaelthas)
+     */
+    public static enum Space {
+        /** A transformation of the bone or spatial in the world space. */
+        CONSTRAINT_SPACE_WORLD,
+        /**
+         * For spatial it is the transformation in its parent space or in WORLD space if it has no parent.
+         * For bone it is a transformation in its bone parent space or in armature space if it has no parent.
+         */
+        CONSTRAINT_SPACE_LOCAL,
+        /**
+         * This space IS NOT applicable for spatials.
+         * For bone it is a transformation in the blender's armature object space.
+         */
+        CONSTRAINT_SPACE_POSE,
+
+        CONSTRAINT_SPACE_PARLOCAL;
+
+        /**
+         * This method returns the enum instance when given the appropriate
+         * value from the blend file.
+         * 
+         * @param c
+         *            the blender's value of the space modifier
+         * @return the scape enum instance
+         */
+        public static Space valueOf(byte c) {
+            switch (c) {
+                case 0:
+                    return CONSTRAINT_SPACE_WORLD;
+                case 1:
+                    return CONSTRAINT_SPACE_LOCAL;
+                case 2:
+                    return CONSTRAINT_SPACE_POSE;
+                case 3:
+                    return CONSTRAINT_SPACE_PARLOCAL;
+                default:
+                    throw new IllegalArgumentException("Value: " + c + " cannot be converted to Space enum instance!");
+            }
+        }
+    }
+}

+ 451 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SimulationNode.java

@@ -0,0 +1,451 @@
+package com.jme3.scene.plugins.blender.constraints;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.animation.AnimChannel;
+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.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.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.objects.ObjectHelper;
+import com.jme3.util.TempVars;
+
+/**
+ * A node that represents either spatial or bone in constraint simulation. The
+ * node is applied its translation, rotation and scale for each frame of its
+ * animation. Then the constraints are applied that will eventually alter it.
+ * After that the feature's transformation is stored in VirtualTrack which is
+ * converted to new bone or spatial track at the very end.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class SimulationNode {
+    private static final Logger  LOGGER   = Logger.getLogger(SimulationNode.class.getName());
+
+    /** The blender context. */
+    private BlenderContext       blenderContext;
+    /** The name of the node (for debugging purposes). */
+    private String               name;
+    /** A list of children for the node (either bones or child spatials). */
+    private List<SimulationNode> children = new ArrayList<SimulationNode>();
+    /** A list of constraints that the current node has. */
+    private List<Constraint>     constraints;
+    /** A list of node's animations. */
+    private List<Animation>      animations;
+
+    /** The nodes spatial (if null then the boneContext should be set). */
+    private Spatial              spatial;
+    /** The skeleton of the bone (not null if the node simulated the bone). */
+    private Skeleton             skeleton;
+    /** Animation controller for the node's feature. */
+    private AnimControl          animControl;
+
+    /**
+     * The star transform of a spatial. Needed to properly reset the spatial to
+     * its start position.
+     */
+    private Transform            spatialStartTransform;
+    /** Star transformations for bones. Needed to properly reset the bones. */
+    private Map<Bone, Transform> boneStartTransforms;
+
+    /**
+     * Builds the nodes tree for the given feature. The feature (bone or
+     * spatial) is found by its OMA. The feature must be a root bone or a root
+     * spatial.
+     * 
+     * @param featureOMA
+     *            the OMA of either bone or spatial
+     * @param blenderContext
+     *            the blender context
+     */
+    public SimulationNode(Long featureOMA, BlenderContext blenderContext) {
+        this(featureOMA, blenderContext, true);
+    }
+
+    /**
+     * Creates the node for the feature.
+     * 
+     * @param featureOMA
+     *            the OMA of either bone or spatial
+     * @param blenderContext
+     *            the blender context
+     * @param rootNode
+     *            indicates if the feature is a root bone or root spatial or not
+     */
+    private SimulationNode(Long featureOMA, BlenderContext blenderContext, boolean rootNode) {
+        this.blenderContext = blenderContext;
+        Node spatial = (Node) blenderContext.getLoadedFeature(featureOMA, LoadedFeatureDataType.LOADED_FEATURE);
+        if (blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, spatial) != null) {
+            skeleton = blenderContext.getSkeleton(featureOMA);
+
+            Node nodeWithAnimationControl = blenderContext.getControlledNode(skeleton);
+            animControl = nodeWithAnimationControl.getControl(AnimControl.class);
+
+            boneStartTransforms = new HashMap<Bone, Transform>();
+            for (int i = 0; i < skeleton.getBoneCount(); ++i) {
+                Bone bone = skeleton.getBone(i);
+                boneStartTransforms.put(bone, new Transform(bone.getWorldBindPosition(), bone.getWorldBindRotation(), bone.getWorldBindScale()));
+            }
+        } else {
+            if (rootNode && spatial.getParent() != null) {
+                throw new IllegalStateException("Given spatial must be a root node!");
+            }
+            this.spatial = spatial;
+            spatialStartTransform = spatial.getLocalTransform().clone();
+        }
+
+        name = '>' + spatial.getName() + '<';
+
+        constraints = this.findConstraints(featureOMA, blenderContext);
+        if (constraints == null) {
+            constraints = new ArrayList<Constraint>();
+        }
+
+        // add children nodes
+        if (skeleton != null) {
+            // bone with index 0 is a root bone and should not be considered
+            // here
+            for (int i = 1; i < skeleton.getBoneCount(); ++i) {
+                BoneContext boneContext = blenderContext.getBoneContext(skeleton.getBone(i));
+                List<Constraint> boneConstraints = this.findConstraints(boneContext.getBoneOma(), blenderContext);
+                if (boneConstraints != null) {
+                    constraints.addAll(boneConstraints);
+                }
+            }
+
+            // each bone of the skeleton has the same anim data applied
+            BoneContext boneContext = blenderContext.getBoneContext(skeleton.getBone(1));
+            Long boneOma = boneContext.getBoneOma();
+            animations = blenderContext.getAnimData(boneOma) == null ? null : blenderContext.getAnimData(boneOma).anims;
+        } else {
+            animations = blenderContext.getAnimData(featureOMA) == null ? null : blenderContext.getAnimData(featureOMA).anims;
+            for (Spatial child : spatial.getChildren()) {
+                if (child instanceof Node) {
+                    children.add(new SimulationNode((Long) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, child), blenderContext, false));
+                }
+            }
+        }
+
+        LOGGER.info("Removing invalid constraints.");
+        List<Constraint> validConstraints = new ArrayList<Constraint>(constraints.size());
+        for (Constraint constraint : constraints) {
+            if (constraint.validate()) {
+                validConstraints.add(constraint);
+            } else {
+                LOGGER.log(Level.WARNING, "Constraint {0} is invalid and will not be applied.", constraint.name);
+            }
+        }
+        constraints = validConstraints;
+    }
+
+    /**
+     * Tells if the node already contains the given constraint (so that it is
+     * not applied twice).
+     * 
+     * @param constraint
+     *            the constraint to be checked
+     * @return <b>true</b> if the constraint already is stored in the node and
+     *         <b>false</b> otherwise
+     */
+    public boolean contains(Constraint constraint) {
+        boolean result = false;
+        if (constraints != null && constraints.size() > 0) {
+            for (Constraint c : constraints) {
+                if (c.equals(constraint)) {
+                    return true;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Resets the node's feature to its starting transformation.
+     */
+    private void reset() {
+        if (spatial != null) {
+            spatial.setLocalTransform(spatialStartTransform);
+            for (SimulationNode child : children) {
+                child.reset();
+            }
+        } else if (skeleton != null) {
+            for (Entry<Bone, Transform> entry : boneStartTransforms.entrySet()) {
+                Transform t = entry.getValue();
+                entry.getKey().setBindTransforms(t.getTranslation(), t.getRotation(), t.getScale());
+            }
+            skeleton.reset();
+        }
+    }
+
+    /**
+     * Simulates the spatial node.
+     */
+    private void simulateSpatial() {
+        if (constraints != null && constraints.size() > 0) {
+            boolean applyStaticConstraints = true;
+            if (animations != null) {
+                for (Animation animation : animations) {
+                    float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation);
+                    int maxFrame = (int) animationTimeBoundaries[0];
+                    float maxTime = animationTimeBoundaries[1];
+
+                    VirtualTrack vTrack = new VirtualTrack(maxFrame, maxTime);
+                    for (Track track : animation.getTracks()) {
+                        for (int frame = 0; frame < maxFrame; ++frame) {
+                            spatial.setLocalTranslation(((SpatialTrack) track).getTranslations()[frame]);
+                            spatial.setLocalRotation(((SpatialTrack) track).getRotations()[frame]);
+                            spatial.setLocalScale(((SpatialTrack) track).getScales()[frame]);
+
+                            for (Constraint constraint : constraints) {
+                                constraint.apply(frame);
+                                vTrack.setTransform(frame, spatial.getLocalTransform());
+                            }
+                        }
+                        Track newTrack = vTrack.getAsSpatialTrack();
+                        if (newTrack != null) {
+                            animation.removeTrack(track);
+                            animation.addTrack(newTrack);
+                        }
+                        applyStaticConstraints = false;
+                    }
+                }
+            }
+
+            // if there are no animations then just constraint the static
+            // object's transformation
+            if (applyStaticConstraints) {
+                for (Constraint constraint : constraints) {
+                    constraint.apply(0);
+                }
+            }
+        }
+
+        for (SimulationNode child : children) {
+            child.simulate();
+        }
+    }
+
+    /**
+     * Simulates the bone node.
+     */
+    private void simulateSkeleton() {
+        if (constraints != null && constraints.size() > 0) {
+            Set<Long> alteredOmas = new HashSet<Long>();
+
+            if (animations != null) {
+                TempVars vars = TempVars.get();
+                AnimChannel animChannel = animControl.createChannel();
+                for (Animation animation : animations) {
+                    float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation);
+                    int maxFrame = (int) animationTimeBoundaries[0];
+                    float maxTime = animationTimeBoundaries[1];
+
+                    Map<Integer, VirtualTrack> tracks = new HashMap<Integer, VirtualTrack>();
+                    Map<Integer, Transform> previousTransforms = this.getInitialTransforms();
+                    for (int frame = 0; frame < maxFrame; ++frame) {
+                        // this MUST be done here, otherwise setting next frame of animation will
+                        // lead to possible errors
+                        this.reset();
+
+                        // first set proper time for all bones in all the tracks ...
+                        for (Track track : animation.getTracks()) {
+                            float time = ((BoneTrack) track).getTimes()[frame];
+                            track.setTime(time, 1, animControl, animChannel, vars);
+                            skeleton.updateWorldVectors();
+                        }
+                        
+
+                        // ... and then apply constraints from the root bone to the last child ...
+                        for (Bone rootBone : skeleton.getRoots()) {
+                            if(skeleton.getBoneIndex(rootBone) > 0) {
+                                //ommit the 0 - indexed root bone as it is the bone added by importer
+                                this.applyConstraints(rootBone, alteredOmas, frame);
+                            }
+                        }
+
+                        // ... add virtual tracks if neccessary, for bones that were altered but had no tracks before ...
+                        for (Long boneOMA : alteredOmas) {
+                            BoneContext boneContext = blenderContext.getBoneContext(boneOMA);
+                            int boneIndex = skeleton.getBoneIndex(boneContext.getBone());
+                            if (!tracks.containsKey(boneIndex)) {
+                                tracks.put(boneIndex, new VirtualTrack(maxFrame, maxTime));
+                            }
+                        }
+                        alteredOmas.clear();
+
+                        // ... and fill in another frame in the result track
+                        for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) {
+                            Integer boneIndex = trackEntry.getKey();
+                            Bone bone = skeleton.getBone(boneIndex);
+
+                            // take the initial transform of a bone and its virtual track
+                            Transform previousTransform = previousTransforms.get(boneIndex);
+                            VirtualTrack vTrack = trackEntry.getValue();
+
+                            Vector3f bonePositionDifference = bone.getLocalPosition().subtract(previousTransform.getTranslation());
+                            Quaternion boneRotationDifference = bone.getLocalRotation().mult(previousTransform.getRotation().inverse()).normalizeLocal();
+                            Vector3f boneScaleDifference = bone.getLocalScale().divide(previousTransform.getScale());
+                            if (frame > 0) {
+                                bonePositionDifference = vTrack.translations.get(frame - 1).add(bonePositionDifference);
+                                boneRotationDifference = vTrack.rotations.get(frame - 1).mult(boneRotationDifference);
+                                boneScaleDifference = vTrack.scales.get(frame - 1).mult(boneScaleDifference);
+                            }
+                            vTrack.setTransform(frame, new Transform(bonePositionDifference, boneRotationDifference, boneScaleDifference));
+
+                            previousTransform.setTranslation(bone.getLocalPosition());
+                            previousTransform.setRotation(bone.getLocalRotation());
+                            previousTransform.setScale(bone.getLocalScale());
+                        }
+                    }
+
+                    for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) {
+                        Track newTrack = trackEntry.getValue().getAsBoneTrack(trackEntry.getKey());
+                        if (newTrack != null) {
+                            boolean trackReplaced = false;
+                            for (Track track : animation.getTracks()) {
+                                if (((BoneTrack) track).getTargetBoneIndex() == trackEntry.getKey().intValue()) {
+                                    animation.removeTrack(track);
+                                    animation.addTrack(newTrack);
+                                    trackReplaced = true;
+                                    break;
+                                }
+                            }
+                            if (!trackReplaced) {
+                                animation.addTrack(newTrack);
+                            }
+                        }
+                    }
+                }
+                vars.release();
+                animControl.clearChannels();
+                this.reset();
+            }
+        }
+    }
+
+    /**
+     * Applies constraints to the given bone and its children.
+     * The goal is to apply constraint from root bone to the last child.
+     * @param bone
+     *            the bone whose constraints will be applied
+     * @param alteredOmas
+     *            the set of OMAS of the altered bones (is populated if necessary)
+     * @param frame
+     *            the current frame of the animation
+     */
+    private void applyConstraints(Bone bone, Set<Long> alteredOmas, int frame) {
+        BoneContext boneContext = blenderContext.getBoneContext(bone);
+        List<Constraint> constraints = this.findConstraints(boneContext.getBoneOma(), blenderContext);
+        if (constraints != null && constraints.size() > 0) {
+            for (Constraint constraint : constraints) {
+                constraint.apply(frame);
+                if (constraint.getAlteredOmas() != null) {
+                    alteredOmas.addAll(constraint.getAlteredOmas());
+                }
+                alteredOmas.add(boneContext.getBoneOma());
+            }
+        }
+        for (Bone child : bone.getChildren()) {
+            this.applyConstraints(child, alteredOmas, frame);
+        }
+    }
+
+    /**
+     * Simulates the node.
+     */
+    public void simulate() {
+        this.reset();
+        if (spatial != null) {
+            this.simulateSpatial();
+        } else {
+            this.simulateSkeleton();
+        }
+    }
+
+    /**
+     * Computes the maximum frame and time for the animation. Different tracks
+     * can have different lengths so here the maximum one is being found.
+     * 
+     * @param animation
+     *            the animation
+     * @return maximum frame and time of the animation
+     */
+    private float[] computeAnimationTimeBoundaries(Animation animation) {
+        int maxFrame = Integer.MIN_VALUE;
+        float maxTime = Float.MIN_VALUE;
+        for (Track track : animation.getTracks()) {
+            if (track instanceof BoneTrack) {
+                maxFrame = Math.max(maxFrame, ((BoneTrack) track).getTranslations().length);
+                maxTime = Math.max(maxTime, ((BoneTrack) track).getTimes()[((BoneTrack) track).getTimes().length - 1]);
+            } else if (track instanceof SpatialTrack) {
+                maxFrame = Math.max(maxFrame, ((SpatialTrack) track).getTranslations().length);
+                maxTime = Math.max(maxTime, ((SpatialTrack) track).getTimes()[((SpatialTrack) track).getTimes().length - 1]);
+            } else {
+                throw new IllegalStateException("Unsupported track type for simuation: " + track);
+            }
+        }
+        return new float[] { maxFrame, maxTime };
+    }
+
+    /**
+     * Finds constraints for the node's features.
+     * 
+     * @param ownerOMA
+     *            the feature's OMA
+     * @param blenderContext
+     *            the blender context
+     * @return a list of feature's constraints or empty list if none were found
+     */
+    private List<Constraint> findConstraints(Long ownerOMA, BlenderContext blenderContext) {
+        List<Constraint> result = new ArrayList<Constraint>();
+        List<Constraint> constraints = blenderContext.getConstraints(ownerOMA);
+        if(constraints != null) {
+            for (Constraint constraint : constraints) {
+                if (constraint.isImplemented() && constraint.validate()) {
+                    result.add(constraint);
+                } else {
+                    LOGGER.log(Level.WARNING, "Constraint named: ''{0}'' of type ''{1}'' is not implemented and will NOT be applied!", new Object[] { constraint.name, constraint.getConstraintTypeName() });
+                }
+            }
+        }
+        return result.size() > 0 ? result : null;
+    }
+
+    /**
+     * Creates the initial transforms for all bones in the skelketon.
+     * @return the map where the key is the bone index and the value us the bone's initial transformation
+     */
+    private Map<Integer, Transform> getInitialTransforms() {
+        Map<Integer, Transform> result = new HashMap<Integer, Transform>();
+        for (int i = 0; i < skeleton.getBoneCount(); ++i) {
+            Bone bone = skeleton.getBone(i);
+            result.put(i, new Transform(bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale()));
+        }
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}

+ 35 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SkeletonConstraint.java

@@ -0,0 +1,35 @@
+package com.jme3.scene.plugins.blender.constraints;
+
+import java.util.logging.Logger;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.animations.Ipo;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * Constraint applied on the skeleton. This constraint is here only to make the
+ * application not crash when loads constraints applied to armature. But
+ * skeleton movement is not supported by jme so the constraint will never be
+ * applied.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class SkeletonConstraint extends Constraint {
+    private static final Logger LOGGER = Logger.getLogger(SkeletonConstraint.class.getName());
+
+    public SkeletonConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
+        super(constraintStructure, ownerOMA, influenceIpo, blenderContext);
+    }
+
+    @Override
+    public boolean validate() {
+        LOGGER.warning("Constraints for skeleton are not supported.");
+        return false;
+    }
+
+    @Override
+    public void apply(int frame) {
+        LOGGER.warning("Applying constraints to skeleton is not supported.");
+    }
+}

+ 27 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SpatialConstraint.java

@@ -0,0 +1,27 @@
+package com.jme3.scene.plugins.blender.constraints;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.animations.Ipo;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * Constraint applied on the spatial objects. This includes: nodes, cameras
+ * nodes and light nodes.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class SpatialConstraint extends Constraint {
+    public SpatialConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
+        super(constraintStructure, ownerOMA, influenceIpo, blenderContext);
+    }
+
+    @Override
+    public boolean validate() {
+        if (targetOMA != null) {
+            return blenderContext.getLoadedFeature(targetOMA, LoadedFeatureDataType.LOADED_FEATURE) != null;
+        }
+        return true;
+    }
+}

+ 146 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/VirtualTrack.java

@@ -0,0 +1,146 @@
+package com.jme3.scene.plugins.blender.constraints;
+
+import java.util.ArrayList;
+
+import com.jme3.animation.BoneTrack;
+import com.jme3.animation.SpatialTrack;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+
+/**
+ * A virtual track that stores computed frames after constraints are applied.
+ * Not all the frames need to be inserted. If there are lacks then the class
+ * will fill the gaps.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class VirtualTrack {
+    /** The last frame for the track. */
+    public int                   maxFrame;
+    /** The max time for the track. */
+    public float                 maxTime;
+    /** Translations of the track. */
+    public ArrayList<Vector3f>   translations;
+    /** Rotations of the track. */
+    public ArrayList<Quaternion> rotations;
+    /** Scales of the track. */
+    public ArrayList<Vector3f>   scales;
+
+    /**
+     * Constructs the object storing the maximum frame and time.
+     * 
+     * @param maxFrame
+     *            the last frame for the track
+     * @param maxTime
+     *            the max time for the track
+     */
+    public VirtualTrack(int maxFrame, float maxTime) {
+        this.maxFrame = maxFrame;
+        this.maxTime = maxTime;
+    }
+
+    /**
+     * Sets the transform for the given frame.
+     * 
+     * @param frameIndex
+     *            the frame for which the transform will be set
+     * @param transform
+     *            the transformation to be set
+     */
+    public void setTransform(int frameIndex, Transform transform) {
+        if (translations == null) {
+            translations = this.createList(Vector3f.ZERO, frameIndex);
+        }
+        this.append(translations, Vector3f.ZERO, frameIndex - translations.size());
+        translations.add(transform.getTranslation().clone());
+
+        if (rotations == null) {
+            rotations = this.createList(Quaternion.IDENTITY, frameIndex);
+        }
+        this.append(rotations, Quaternion.IDENTITY, frameIndex - rotations.size());
+        rotations.add(transform.getRotation().clone());
+
+        if (scales == null) {
+            scales = this.createList(Vector3f.UNIT_XYZ, frameIndex);
+        }
+        this.append(scales, Vector3f.UNIT_XYZ, frameIndex - scales.size());
+        scales.add(transform.getScale().clone());
+    }
+
+    /**
+     * Returns the track as a bone track.
+     * 
+     * @param targetBoneIndex
+     *            the bone index
+     * @return the bone track
+     */
+    public BoneTrack getAsBoneTrack(int targetBoneIndex) {
+        if (translations == null && rotations == null && scales == null) {
+            return null;
+        }
+        return new BoneTrack(targetBoneIndex, this.createTimes(), translations.toArray(new Vector3f[maxFrame]), rotations.toArray(new Quaternion[maxFrame]), scales.toArray(new Vector3f[maxFrame]));
+    }
+
+    /**
+     * Returns the track as a spatial track.
+     * 
+     * @return the spatial track
+     */
+    public SpatialTrack getAsSpatialTrack() {
+        if (translations == null && rotations == null && scales == null) {
+            return null;
+        }
+        return new SpatialTrack(this.createTimes(), translations.toArray(new Vector3f[maxFrame]), rotations.toArray(new Quaternion[maxFrame]), scales.toArray(new Vector3f[maxFrame]));
+    }
+
+    /**
+     * The method creates times for the track based on the given maximum values.
+     * 
+     * @return the times for the track
+     */
+    private float[] createTimes() {
+        float[] times = new float[maxFrame];
+        float dT = maxTime / (float) maxFrame;
+        float t = 0;
+        for (int i = 0; i < maxFrame; ++i) {
+            times[i] = t;
+            t += dT;
+        }
+        return times;
+    }
+
+    /**
+     * Helper method that creates a list of a given size filled with given
+     * elements.
+     * 
+     * @param element
+     *            the element to be put into the list
+     * @param count
+     *            the list size
+     * @return the list
+     */
+    private <T> ArrayList<T> createList(T element, int count) {
+        ArrayList<T> result = new ArrayList<T>(count);
+        for (int i = 0; i < count; ++i) {
+            result.add(element);
+        }
+        return result;
+    }
+
+    /**
+     * Appends the element to the given list.
+     * 
+     * @param list
+     *            the list where the element will be appended
+     * @param element
+     *            the element to be appended
+     * @param count
+     *            how many times the element will be appended
+     */
+    private <T> void append(ArrayList<T> list, T element, int count) {
+        for (int i = 0; i < count; ++i) {
+            list.add(element);
+        }
+    }
+}

+ 135 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinition.java

@@ -0,0 +1,135 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import java.util.Set;
+
+import com.jme3.animation.Bone;
+import com.jme3.math.Transform;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * A base class for all constraint definitions.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public abstract class ConstraintDefinition {
+    protected ConstraintHelper constraintHelper;
+    /** Constraints flag. Used to load user's options applied to the constraint. */
+    protected int              flag;
+    /** The constraint's owner. Loaded during runtime. */
+    private Object             owner;
+    /** The blender context. */
+    protected BlenderContext   blenderContext;
+    /** The constraint's owner OMA. */
+    protected Long             ownerOMA;
+    /** Stores the OMA addresses of all features whose transform had been altered beside the constraint owner. */
+    protected Set<Long>        alteredOmas;
+
+    /**
+     * Loads a constraint definition based on the constraint definition
+     * structure.
+     * 
+     * @param constraintData
+     *            the constraint definition structure
+     * @param ownerOMA
+     *            the constraint's owner OMA
+     * @param blenderContext
+     *            the blender context
+     */
+    public ConstraintDefinition(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        if (constraintData != null) {// Null constraint has no data
+            Number flag = (Number) constraintData.getFieldValue("flag");
+            if (flag != null) {
+                this.flag = flag.intValue();
+            }
+        }
+        this.blenderContext = blenderContext;
+        constraintHelper = (ConstraintHelper) (blenderContext == null ? null : blenderContext.getHelper(ConstraintHelper.class));
+        this.ownerOMA = ownerOMA;
+    }
+
+    /**
+     * This method is here because we have no guarantee that the owner is loaded
+     * when constraint is being created. So use it to get the owner when it is
+     * needed for computations.
+     * 
+     * @return the owner of the constraint or null if none is set
+     */
+    protected Object getOwner() {
+        if (ownerOMA != null && owner == null) {
+            owner = blenderContext.getLoadedFeature(ownerOMA, LoadedFeatureDataType.LOADED_FEATURE);
+            if (owner == null) {
+                throw new IllegalStateException("Cannot load constraint's owner for constraint type: " + this.getClass().getName());
+            }
+        }
+        return owner;
+    }
+
+    /**
+     * The method gets the owner's transformation. The owner can be either bone or spatial.
+     * @param ownerSpace
+     *            the space in which the computed transformation is given
+     * @return the constraint owner's transformation
+     */
+    protected Transform getOwnerTransform(Space ownerSpace) {
+        if (this.getOwner() instanceof Bone) {
+            BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
+            return constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace);
+        }
+        return constraintHelper.getTransform(ownerOMA, null, ownerSpace);
+    }
+
+    /**
+     * The method applies the given transformation to the owner.
+     * @param ownerTransform
+     *            the transformation to apply to the owner
+     * @param ownerSpace
+     *            the space that defines which owner's transformation (ie. global, local, etc. will be set)
+     */
+    protected void applyOwnerTransform(Transform ownerTransform, Space ownerSpace) {
+        if (this.getOwner() instanceof Bone) {
+            BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
+            constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace, ownerTransform);
+        } else {
+            constraintHelper.applyTransform(ownerOMA, null, ownerSpace, ownerTransform);
+        }
+    }
+
+    /**
+     * @return <b>true</b> if the definition is implemented and <b>false</b>
+     *         otherwise
+     */
+    public boolean isImplemented() {
+        return true;
+    }
+
+    /**
+     * @return a list of all OMAs of the features that the constraint had altered beside its owner
+     */
+    public Set<Long> getAlteredOmas() {
+        return alteredOmas;
+    }
+
+    /**
+     * @return the type name of the constraint
+     */
+    public abstract String getConstraintTypeName();
+
+    /**
+     * Bakes the constraint for the current feature (bone or spatial) position.
+     * 
+     * @param ownerSpace
+     *            the space where owner transform will be evaluated in
+     * @param targetSpace
+     *            the space where target transform will be evaluated in
+     * @param targetTransform
+     *            the target transform used by some of the constraints
+     * @param influence
+     *            the influence of the constraint (from range <0; 1>)
+     */
+    public abstract void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence);
+}

+ 77 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionDistLimit.java

@@ -0,0 +1,77 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.animation.Bone;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Dist limit' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionDistLimit extends ConstraintDefinition {
+    private static final int LIMITDIST_INSIDE    = 0;
+    private static final int LIMITDIST_OUTSIDE   = 1;
+    private static final int LIMITDIST_ONSURFACE = 2;
+
+    protected int            mode;
+    protected float          dist;
+
+    public ConstraintDefinitionDistLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+        mode = ((Number) constraintData.getFieldValue("mode")).intValue();
+        dist = ((Number) constraintData.getFieldValue("dist")).floatValue();
+    }
+    
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        if (this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null &&
+            blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)) {
+            // distance limit does not work on bones who are connected to their parent
+            return;
+        }
+        
+        Transform ownerTransform = this.getOwnerTransform(ownerSpace);
+
+        Vector3f v = ownerTransform.getTranslation().subtract(targetTransform.getTranslation());
+        float currentDistance = v.length();
+        switch (mode) {
+            case LIMITDIST_INSIDE:
+                if (currentDistance >= dist) {
+                    v.normalizeLocal();
+                    v.multLocal(dist + (currentDistance - dist) * (1.0f - influence));
+                    ownerTransform.getTranslation().set(v.addLocal(targetTransform.getTranslation()));
+                }
+                break;
+            case LIMITDIST_ONSURFACE:
+                if (currentDistance > dist) {
+                    v.normalizeLocal();
+                    v.multLocal(dist + (currentDistance - dist) * (1.0f - influence));
+                    ownerTransform.getTranslation().set(v.addLocal(targetTransform.getTranslation()));
+                } else if (currentDistance < dist) {
+                    v.normalizeLocal().multLocal(dist * influence);
+                    ownerTransform.getTranslation().set(targetTransform.getTranslation().add(v));
+                }
+                break;
+            case LIMITDIST_OUTSIDE:
+                if (currentDistance <= dist) {
+                    v = targetTransform.getTranslation().subtract(ownerTransform.getTranslation()).normalizeLocal().multLocal(dist * influence);
+                    ownerTransform.getTranslation().set(targetTransform.getTranslation().add(v));
+                }
+                break;
+            default:
+                throw new IllegalStateException("Unknown distance limit constraint mode: " + mode);
+        }
+
+        this.applyOwnerTransform(ownerTransform, ownerSpace);
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Limit distance";
+    }
+}

+ 124 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionFactory.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.constraints.definitions;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+public class ConstraintDefinitionFactory {
+    private static final Map<String, Class<? extends ConstraintDefinition>> CONSTRAINT_CLASSES      = new HashMap<String, Class<? extends ConstraintDefinition>>();
+    static {
+        CONSTRAINT_CLASSES.put("bDistLimitConstraint", ConstraintDefinitionDistLimit.class);
+        CONSTRAINT_CLASSES.put("bLocateLikeConstraint", ConstraintDefinitionLocLike.class);
+        CONSTRAINT_CLASSES.put("bLocLimitConstraint", ConstraintDefinitionLocLimit.class);
+        CONSTRAINT_CLASSES.put("bNullConstraint", ConstraintDefinitionNull.class);
+        CONSTRAINT_CLASSES.put("bRotateLikeConstraint", ConstraintDefinitionRotLike.class);
+        CONSTRAINT_CLASSES.put("bRotLimitConstraint", ConstraintDefinitionRotLimit.class);
+        CONSTRAINT_CLASSES.put("bSizeLikeConstraint", ConstraintDefinitionSizeLike.class);
+        CONSTRAINT_CLASSES.put("bSizeLimitConstraint", ConstraintDefinitionSizeLimit.class);
+        CONSTRAINT_CLASSES.put("bKinematicConstraint", ConstraintDefinitionIK.class);
+    }
+
+    private static final Map<String, String>                                UNSUPPORTED_CONSTRAINTS = new HashMap<String, String>();
+    static {
+        UNSUPPORTED_CONSTRAINTS.put("bActionConstraint", "Action");
+        UNSUPPORTED_CONSTRAINTS.put("bChildOfConstraint", "Child of");
+        UNSUPPORTED_CONSTRAINTS.put("bClampToConstraint", "Clamp to");
+        UNSUPPORTED_CONSTRAINTS.put("bFollowPathConstraint", "Follow path");
+        UNSUPPORTED_CONSTRAINTS.put("bLockTrackConstraint", "Lock track");
+        UNSUPPORTED_CONSTRAINTS.put("bMinMaxConstraint", "Min max");
+        UNSUPPORTED_CONSTRAINTS.put("bPythonConstraint", "Python/Script");
+        UNSUPPORTED_CONSTRAINTS.put("bRigidBodyJointConstraint", "Rigid body joint");
+        UNSUPPORTED_CONSTRAINTS.put("bShrinkWrapConstraint", "Shrinkwrap");
+        UNSUPPORTED_CONSTRAINTS.put("bStretchToConstraint", "Stretch to");
+        UNSUPPORTED_CONSTRAINTS.put("bTransformConstraint", "Transform");
+        // Blender 2.50+
+        UNSUPPORTED_CONSTRAINTS.put("bSplineIKConstraint", "Spline inverse kinematics");
+        UNSUPPORTED_CONSTRAINTS.put("bDampTrackConstraint", "Damp track");
+        UNSUPPORTED_CONSTRAINTS.put("bPivotConstraint", "Pivot");
+        // Blender 2.56+
+        UNSUPPORTED_CONSTRAINTS.put("bTrackToConstraint", "Track to");
+        UNSUPPORTED_CONSTRAINTS.put("bSameVolumeConstraint", "Same volume");
+        UNSUPPORTED_CONSTRAINTS.put("bTransLikeConstraint", "Trans like");
+        // Blender 2.62+
+        UNSUPPORTED_CONSTRAINTS.put("bCameraSolverConstraint", "Camera solver");
+        UNSUPPORTED_CONSTRAINTS.put("bObjectSolverConstraint", "Object solver");
+        UNSUPPORTED_CONSTRAINTS.put("bFollowTrackConstraint", "Follow track");
+    }
+
+    /**
+     * This method creates the constraint instance.
+     * 
+     * @param constraintStructure
+     *            the constraint's structure (bConstraint clss in blender 2.49).
+     *            If the value is null the NullConstraint is created.
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             this exception is thrown when the blender file is somehow
+     *             corrupted
+     */
+    public static ConstraintDefinition createConstraintDefinition(Structure constraintStructure, Long ownerOMA, BlenderContext blenderContext) throws BlenderFileException {
+        if (constraintStructure == null) {
+            return new ConstraintDefinitionNull(null, ownerOMA, blenderContext);
+        }
+        String constraintClassName = constraintStructure.getType();
+        Class<? extends ConstraintDefinition> constraintDefinitionClass = CONSTRAINT_CLASSES.get(constraintClassName);
+        if (constraintDefinitionClass != null) {
+            try {
+                return (ConstraintDefinition) constraintDefinitionClass.getDeclaredConstructors()[0].newInstance(constraintStructure, ownerOMA, blenderContext);
+            } catch (IllegalArgumentException e) {
+                throw new BlenderFileException(e.getLocalizedMessage(), e);
+            } catch (SecurityException e) {
+                throw new BlenderFileException(e.getLocalizedMessage(), e);
+            } catch (InstantiationException e) {
+                throw new BlenderFileException(e.getLocalizedMessage(), e);
+            } catch (IllegalAccessException e) {
+                throw new BlenderFileException(e.getLocalizedMessage(), e);
+            } catch (InvocationTargetException e) {
+                throw new BlenderFileException(e.getLocalizedMessage(), e);
+            }
+        } else {
+            String constraintName = UNSUPPORTED_CONSTRAINTS.get(constraintClassName);
+            if (constraintName != null) {
+                return new UnsupportedConstraintDefinition(constraintName);
+            } else {
+                throw new BlenderFileException("Unknown constraint type: " + constraintClassName);
+            }
+        }
+    }
+}

+ 117 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java

@@ -0,0 +1,117 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import com.jme3.animation.Bone;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+public class ConstraintDefinitionIK extends ConstraintDefinition {
+
+    private static final int FLAG_POSITION = 0x20;
+
+    /** The number of affected bones. Zero means that all parent bones of the current bone should take part in baking. */
+    private int              bonesAffected;
+    private float            chainLength;
+    private BoneContext[]    bones;
+    private boolean          needToCompute = true;
+
+    public ConstraintDefinitionIK(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+        bonesAffected = ((Number) constraintData.getFieldValue("rootbone")).intValue();
+
+        if ((flag & FLAG_POSITION) == 0) {
+            needToCompute = false;
+        }
+
+        if (needToCompute) {
+            alteredOmas = new HashSet<Long>();
+        }
+    }
+    
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        if (needToCompute && influence != 0) {
+            ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
+            BoneContext[] boneContexts = this.getBones();
+            float b = chainLength;
+            Quaternion boneWorldRotation = new Quaternion();
+
+            for (int i = 0; i < boneContexts.length; ++i) {
+                Bone bone = boneContexts[i].getBone();
+
+                bone.updateWorldVectors();
+                Transform boneWorldTransform = constraintHelper.getTransform(boneContexts[i].getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD);
+
+                Vector3f head = boneWorldTransform.getTranslation();
+                Vector3f tail = head.add(bone.getModelSpaceRotation().mult(Vector3f.UNIT_Y.mult(boneContexts[i].getLength())));
+
+                Vector3f vectorA = tail.subtract(head);
+                float a = vectorA.length();
+                vectorA.normalizeLocal();
+
+                Vector3f vectorC = targetTransform.getTranslation().subtract(head);
+                float c = vectorC.length();
+                vectorC.normalizeLocal();
+
+                b -= a;
+                float theta = 0;
+
+                if (c >= a + b) {
+                    theta = vectorA.angleBetween(vectorC);
+                } else if (c <= FastMath.abs(a - b) && i < boneContexts.length - 1) {
+                    theta = vectorA.angleBetween(vectorC) - FastMath.HALF_PI;
+                } else {
+                    theta = vectorA.angleBetween(vectorC) - FastMath.acos(-(b * b - a * a - c * c) / (2 * a * c));
+                }
+                
+                theta *= influence;
+
+                if (theta != 0) {
+                    Vector3f vectorR = vectorA.cross(vectorC);
+                    boneWorldRotation.fromAngleAxis(theta, vectorR);
+                    boneWorldTransform.getRotation().multLocal(boneWorldRotation);
+                    constraintHelper.applyTransform(boneContexts[i].getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD, boneWorldTransform);
+                }
+
+                bone.updateWorldVectors();
+                alteredOmas.add(boneContexts[i].getBoneOma());
+            }
+        }
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Inverse kinematics";
+    }
+
+    /**
+     * @return the bone contexts of all bones that will be used in this constraint computations
+     */
+    private BoneContext[] getBones() {
+        if (bones == null) {
+            List<BoneContext> bones = new ArrayList<BoneContext>();
+            Bone bone = (Bone) this.getOwner();
+            while (bone != null) {
+                BoneContext boneContext = blenderContext.getBoneContext(bone);
+                bones.add(0, boneContext);
+                chainLength += boneContext.getLength();
+                if (bonesAffected != 0 && bones.size() >= bonesAffected) {
+                    break;
+                }
+                bone = bone.getParent();
+            }
+            this.bones = bones.toArray(new BoneContext[bones.size()]);
+        }
+        return bones;
+    }
+}

+ 96 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLike.java

@@ -0,0 +1,96 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.animation.Bone;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Loc like' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionLocLike extends ConstraintDefinition {
+    private static final int LOCLIKE_X        = 0x01;
+    private static final int LOCLIKE_Y        = 0x02;
+    private static final int LOCLIKE_Z        = 0x04;
+    // protected static final int LOCLIKE_TIP = 0x08;//this is deprecated in
+    // blender
+    private static final int LOCLIKE_X_INVERT = 0x10;
+    private static final int LOCLIKE_Y_INVERT = 0x20;
+    private static final int LOCLIKE_Z_INVERT = 0x40;
+    private static final int LOCLIKE_OFFSET   = 0x80;
+
+    public ConstraintDefinitionLocLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+        if (blenderContext.getBlenderKey().isFixUpAxis()) {
+            // swapping Y and X limits flag in the bitwise flag
+            int y = flag & LOCLIKE_Y;
+            int invY = flag & LOCLIKE_Y_INVERT;
+            int z = flag & LOCLIKE_Z;
+            int invZ = flag & LOCLIKE_Z_INVERT;
+            // clear the other flags to swap them
+            flag &= LOCLIKE_X | LOCLIKE_X_INVERT | LOCLIKE_OFFSET;
+            
+            flag |= y << 1;
+            flag |= invY << 1;
+            flag |= z >> 1;
+            flag |= invZ >> 1;
+        }
+    }
+    
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        if (this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null &&
+            blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)) {
+            // location copy does not work on bones who are connected to their parent
+            return;
+        }
+        
+        Transform ownerTransform = this.getOwnerTransform(ownerSpace);
+        
+        Vector3f ownerLocation = ownerTransform.getTranslation();
+        Vector3f targetLocation = targetTransform.getTranslation();
+
+        Vector3f startLocation = ownerTransform.getTranslation().clone();
+        Vector3f offset = Vector3f.ZERO;
+        if ((flag & LOCLIKE_OFFSET) != 0) {// we add the original location to the copied location
+            offset = startLocation;
+        }
+
+        if ((flag & LOCLIKE_X) != 0) {
+            ownerLocation.x = targetLocation.x;
+            if ((flag & LOCLIKE_X_INVERT) != 0) {
+                ownerLocation.x = -ownerLocation.x;
+            }
+        }
+        if ((flag & LOCLIKE_Y) != 0) {
+            ownerLocation.y = targetLocation.y;
+            if ((flag & LOCLIKE_Y_INVERT) != 0) {
+                ownerLocation.y = -ownerLocation.y;
+            }
+        }
+        if ((flag & LOCLIKE_Z) != 0) {
+            ownerLocation.z = targetLocation.z;
+            if ((flag & LOCLIKE_Z_INVERT) != 0) {
+                ownerLocation.z = -ownerLocation.z;
+            }
+        }
+        ownerLocation.addLocal(offset);
+
+        if (influence < 1.0f) {
+            startLocation.subtractLocal(ownerLocation).normalizeLocal().mult(influence);
+            ownerLocation.addLocal(startLocation);
+        }
+        
+        this.applyOwnerTransform(ownerTransform, ownerSpace);
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Copy location";
+    }
+}

+ 95 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLimit.java

@@ -0,0 +1,95 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.animation.Bone;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.animations.BoneContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Loc limit' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionLocLimit extends ConstraintDefinition {
+    private static final int LIMIT_XMIN = 0x01;
+    private static final int LIMIT_XMAX = 0x02;
+    private static final int LIMIT_YMIN = 0x04;
+    private static final int LIMIT_YMAX = 0x08;
+    private static final int LIMIT_ZMIN = 0x10;
+    private static final int LIMIT_ZMAX = 0x20;
+
+    protected float[][]      limits     = new float[3][2];
+
+    public ConstraintDefinitionLocLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+        if (blenderContext.getBlenderKey().isFixUpAxis()) {
+            limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
+            limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
+            limits[2][0] = -((Number) constraintData.getFieldValue("ymin")).floatValue();
+            limits[2][1] = -((Number) constraintData.getFieldValue("ymax")).floatValue();
+            limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
+            limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
+
+            // swapping Y and X limits flag in the bitwise flag
+            int ymin = flag & LIMIT_YMIN;
+            int ymax = flag & LIMIT_YMAX;
+            int zmin = flag & LIMIT_ZMIN;
+            int zmax = flag & LIMIT_ZMAX;
+            flag &= LIMIT_XMIN | LIMIT_XMAX;// clear the other flags to swap
+                                            // them
+            flag |= ymin << 2;
+            flag |= ymax << 2;
+            flag |= zmin >> 2;
+            flag |= zmax >> 2;
+        } else {
+            limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
+            limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
+            limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
+            limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
+            limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
+            limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
+        }
+    }
+    
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        if (this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null &&
+            blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)) {
+            // location limit does not work on bones who are connected to their parent
+            return;
+        }
+        
+        Transform ownerTransform = this.getOwnerTransform(ownerSpace);
+        
+        Vector3f translation = ownerTransform.getTranslation();
+
+        if ((flag & LIMIT_XMIN) != 0 && translation.x < limits[0][0]) {
+            translation.x -= (translation.x - limits[0][0]) * influence;
+        }
+        if ((flag & LIMIT_XMAX) != 0 && translation.x > limits[0][1]) {
+            translation.x -= (translation.x - limits[0][1]) * influence;
+        }
+        if ((flag & LIMIT_YMIN) != 0 && translation.y < limits[1][0]) {
+            translation.y -= (translation.y - limits[1][0]) * influence;
+        }
+        if ((flag & LIMIT_YMAX) != 0 && translation.y > limits[1][1]) {
+            translation.y -= (translation.y - limits[1][1]) * influence;
+        }
+        if ((flag & LIMIT_ZMIN) != 0 && translation.z < limits[2][0]) {
+            translation.z -= (translation.z - limits[2][0]) * influence;
+        }
+        if ((flag & LIMIT_ZMAX) != 0 && translation.z > limits[2][1]) {
+            translation.z -= (translation.z - limits[2][1]) * influence;
+        }
+        
+        this.applyOwnerTransform(ownerTransform, ownerSpace);
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Limit location";
+    }
+}

+ 28 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionNull.java

@@ -0,0 +1,28 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.math.Transform;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Null' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionNull extends ConstraintDefinition {
+
+    public ConstraintDefinitionNull(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+    }
+
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        // null constraint does nothing so no need to implement this one
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Null";
+    }
+}

+ 78 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLike.java

@@ -0,0 +1,78 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Rot like' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionRotLike extends ConstraintDefinition {
+    private static final int  ROTLIKE_X        = 0x01;
+    private static final int  ROTLIKE_Y        = 0x02;
+    private static final int  ROTLIKE_Z        = 0x04;
+    private static final int  ROTLIKE_X_INVERT = 0x10;
+    private static final int  ROTLIKE_Y_INVERT = 0x20;
+    private static final int  ROTLIKE_Z_INVERT = 0x40;
+    private static final int  ROTLIKE_OFFSET   = 0x80;
+
+    private transient float[] ownerAngles      = new float[3];
+    private transient float[] targetAngles     = new float[3];
+
+    public ConstraintDefinitionRotLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+    }
+
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        Transform ownerTransform = this.getOwnerTransform(ownerSpace);
+        
+        Quaternion ownerRotation = ownerTransform.getRotation();
+        ownerAngles = ownerRotation.toAngles(ownerAngles);
+        targetAngles = targetTransform.getRotation().toAngles(targetAngles);
+
+        Quaternion startRotation = ownerRotation.clone();
+        Quaternion offset = Quaternion.IDENTITY;
+        if ((flag & ROTLIKE_OFFSET) != 0) {// we add the original rotation to
+                                           // the copied rotation
+            offset = startRotation;
+        }
+
+        if ((flag & ROTLIKE_X) != 0) {
+            ownerAngles[0] = targetAngles[0];
+            if ((flag & ROTLIKE_X_INVERT) != 0) {
+                ownerAngles[0] = -ownerAngles[0];
+            }
+        }
+        if ((flag & ROTLIKE_Y) != 0) {
+            ownerAngles[1] = targetAngles[1];
+            if ((flag & ROTLIKE_Y_INVERT) != 0) {
+                ownerAngles[1] = -ownerAngles[1];
+            }
+        }
+        if ((flag & ROTLIKE_Z) != 0) {
+            ownerAngles[2] = targetAngles[2];
+            if ((flag & ROTLIKE_Z_INVERT) != 0) {
+                ownerAngles[2] = -ownerAngles[2];
+            }
+        }
+        ownerRotation.fromAngles(ownerAngles).multLocal(offset);
+
+        if (influence < 1.0f) {
+            // startLocation.subtractLocal(ownerLocation).normalizeLocal().mult(influence);
+            // ownerLocation.addLocal(startLocation);
+            // TODO
+        }
+        
+        this.applyOwnerTransform(ownerTransform, ownerSpace);
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Copy rotation";
+    }
+}

+ 119 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLimit.java

@@ -0,0 +1,119 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.math.FastMath;
+import com.jme3.math.Transform;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Rot limit' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionRotLimit extends ConstraintDefinition {
+    private static final int    LIMIT_XROT = 0x01;
+    private static final int    LIMIT_YROT = 0x02;
+    private static final int    LIMIT_ZROT = 0x04;
+
+    private transient float[][] limits     = new float[3][2];
+    private transient float[]   angles     = new float[3];
+
+    public ConstraintDefinitionRotLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+        if (blenderContext.getBlenderKey().isFixUpAxis()) {
+            limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
+            limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
+            limits[2][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
+            limits[2][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
+            limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
+            limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
+
+            // swapping Y and X limits flag in the bitwise flag
+            int limitY = flag & LIMIT_YROT;
+            int limitZ = flag & LIMIT_ZROT;
+            flag &= LIMIT_XROT;// clear the other flags to swap them
+            flag |= limitY << 1;
+            flag |= limitZ >> 1;
+        } else {
+            limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
+            limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
+            limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
+            limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
+            limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
+            limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
+        }
+
+        // until blender 2.49 the rotations values were stored in degrees
+        if (blenderContext.getBlenderVersion() <= 249) {
+            for (int i = 0; i < 3; ++i) {
+                limits[i][0] *= FastMath.DEG_TO_RAD;
+                limits[i][1] *= FastMath.DEG_TO_RAD;
+            }
+        }
+
+        // make sure that the limits are always in range [0, 2PI)
+        // TODO: left it here because it is essential to make sure all cases
+        // work poperly
+        // but will do it a little bit later ;)
+        /*
+         * for (int i = 0; i < 3; ++i) { for (int j = 0; j < 2; ++j) { int
+         * multFactor = (int)Math.abs(limits[i][j] / FastMath.TWO_PI) ; if
+         * (limits[i][j] < 0) { limits[i][j] += FastMath.TWO_PI * (multFactor +
+         * 1); } else { limits[i][j] -= FastMath.TWO_PI * multFactor; } } //make
+         * sure the lower limit is not greater than the upper one
+         * if(limits[i][0] > limits[i][1]) { float temp = limits[i][0];
+         * limits[i][0] = limits[i][1]; limits[i][1] = temp; } }
+         */
+    }
+    
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        Transform ownerTransform = this.getOwnerTransform(ownerSpace);
+        
+        ownerTransform.getRotation().toAngles(angles);
+        // make sure that the rotations are always in range [0, 2PI)
+        // TODO: same comment as in constructor
+        /*
+         * for (int i = 0; i < 3; ++i) { int multFactor =
+         * (int)Math.abs(angles[i] / FastMath.TWO_PI) ; if(angles[i] < 0) {
+         * angles[i] += FastMath.TWO_PI * (multFactor + 1); } else { angles[i]
+         * -= FastMath.TWO_PI * multFactor; } }
+         */
+        if ((flag & LIMIT_XROT) != 0) {
+            float difference = 0.0f;
+            if (angles[0] < limits[0][0]) {
+                difference = (angles[0] - limits[0][0]) * influence;
+            } else if (angles[0] > limits[0][1]) {
+                difference = (angles[0] - limits[0][1]) * influence;
+            }
+            angles[0] -= difference;
+        }
+        if ((flag & LIMIT_YROT) != 0) {
+            float difference = 0.0f;
+            if (angles[1] < limits[1][0]) {
+                difference = (angles[1] - limits[1][0]) * influence;
+            } else if (angles[1] > limits[1][1]) {
+                difference = (angles[1] - limits[1][1]) * influence;
+            }
+            angles[1] -= difference;
+        }
+        if ((flag & LIMIT_ZROT) != 0) {
+            float difference = 0.0f;
+            if (angles[2] < limits[2][0]) {
+                difference = (angles[2] - limits[2][0]) * influence;
+            } else if (angles[2] > limits[2][1]) {
+                difference = (angles[2] - limits[2][1]) * influence;
+            }
+            angles[2] -= difference;
+        }
+        ownerTransform.getRotation().fromAngles(angles);
+        
+        this.applyOwnerTransform(ownerTransform, ownerSpace);
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Limit rotation";
+    }
+}

+ 64 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLike.java

@@ -0,0 +1,64 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Size like' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionSizeLike extends ConstraintDefinition {
+    private static final int SIZELIKE_X     = 0x01;
+    private static final int SIZELIKE_Y     = 0x02;
+    private static final int SIZELIKE_Z     = 0x04;
+    private static final int LOCLIKE_OFFSET = 0x80;
+
+    public ConstraintDefinitionSizeLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+        if (blenderContext.getBlenderKey().isFixUpAxis()) {
+            // swapping Y and X limits flag in the bitwise flag
+            int y = flag & SIZELIKE_Y;
+            int z = flag & SIZELIKE_Z;
+            flag &= SIZELIKE_X | LOCLIKE_OFFSET;// clear the other flags to swap
+                                                // them
+            flag |= y << 1;
+            flag |= z >> 1;
+        }
+    }
+    
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        Transform ownerTransform = this.getOwnerTransform(ownerSpace);
+        
+        Vector3f ownerScale = ownerTransform.getScale();
+        Vector3f targetScale = targetTransform.getScale();
+
+        Vector3f offset = Vector3f.ZERO;
+        if ((flag & LOCLIKE_OFFSET) != 0) {// we add the original scale to the
+                                           // copied scale
+            offset = ownerScale.clone();
+        }
+
+        if ((flag & SIZELIKE_X) != 0) {
+            ownerScale.x = targetScale.x * influence + (1.0f - influence) * ownerScale.x;
+        }
+        if ((flag & SIZELIKE_Y) != 0) {
+            ownerScale.y = targetScale.y * influence + (1.0f - influence) * ownerScale.y;
+        }
+        if ((flag & SIZELIKE_Z) != 0) {
+            ownerScale.z = targetScale.z * influence + (1.0f - influence) * ownerScale.z;
+        }
+        ownerScale.addLocal(offset);
+        
+        this.applyOwnerTransform(ownerTransform, ownerSpace);
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Copy scale";
+    }
+}

+ 86 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLimit.java

@@ -0,0 +1,86 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * This class represents 'Size limit' constraint type in blender.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class ConstraintDefinitionSizeLimit extends ConstraintDefinition {
+    private static final int      LIMIT_XMIN = 0x01;
+    private static final int      LIMIT_XMAX = 0x02;
+    private static final int      LIMIT_YMIN = 0x04;
+    private static final int      LIMIT_YMAX = 0x08;
+    private static final int      LIMIT_ZMIN = 0x10;
+    private static final int      LIMIT_ZMAX = 0x20;
+
+    protected transient float[][] limits     = new float[3][2];
+
+    public ConstraintDefinitionSizeLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
+        super(constraintData, ownerOMA, blenderContext);
+        if (blenderContext.getBlenderKey().isFixUpAxis()) {
+            limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
+            limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
+            limits[2][0] = -((Number) constraintData.getFieldValue("ymin")).floatValue();
+            limits[2][1] = -((Number) constraintData.getFieldValue("ymax")).floatValue();
+            limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
+            limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
+
+            // swapping Y and X limits flag in the bitwise flag
+            int ymin = flag & LIMIT_YMIN;
+            int ymax = flag & LIMIT_YMAX;
+            int zmin = flag & LIMIT_ZMIN;
+            int zmax = flag & LIMIT_ZMAX;
+            flag &= LIMIT_XMIN | LIMIT_XMAX;// clear the other flags to swap
+                                            // them
+            flag |= ymin << 2;
+            flag |= ymax << 2;
+            flag |= zmin >> 2;
+            flag |= zmax >> 2;
+        } else {
+            limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
+            limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
+            limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
+            limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
+            limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
+            limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
+        }
+    }
+    
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+        Transform ownerTransform = this.getOwnerTransform(ownerSpace);
+        
+        Vector3f scale = ownerTransform.getScale();
+        if ((flag & LIMIT_XMIN) != 0 && scale.x < limits[0][0]) {
+            scale.x -= (scale.x - limits[0][0]) * influence;
+        }
+        if ((flag & LIMIT_XMAX) != 0 && scale.x > limits[0][1]) {
+            scale.x -= (scale.x - limits[0][1]) * influence;
+        }
+        if ((flag & LIMIT_YMIN) != 0 && scale.y < limits[1][0]) {
+            scale.y -= (scale.y - limits[1][0]) * influence;
+        }
+        if ((flag & LIMIT_YMAX) != 0 && scale.y > limits[1][1]) {
+            scale.y -= (scale.y - limits[1][1]) * influence;
+        }
+        if ((flag & LIMIT_ZMIN) != 0 && scale.z < limits[2][0]) {
+            scale.z -= (scale.z - limits[2][0]) * influence;
+        }
+        if ((flag & LIMIT_ZMAX) != 0 && scale.z > limits[2][1]) {
+            scale.z -= (scale.z - limits[2][1]) * influence;
+        }
+        
+        this.applyOwnerTransform(ownerTransform, ownerSpace);
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return "Limit scale";
+    }
+}

+ 34 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/UnsupportedConstraintDefinition.java

@@ -0,0 +1,34 @@
+package com.jme3.scene.plugins.blender.constraints.definitions;
+
+import com.jme3.math.Transform;
+import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
+
+/**
+ * This class represents a constraint that is defined by blender but not
+ * supported by either importer ot jme. It only wirtes down a warning when
+ * baking is called.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */class UnsupportedConstraintDefinition extends ConstraintDefinition {
+    private String typeName;
+
+    public UnsupportedConstraintDefinition(String typeName) {
+        super(null, null, null);
+        this.typeName = typeName;
+    }
+
+    @Override
+    public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
+    }
+
+    @Override
+    public boolean isImplemented() {
+        return false;
+    }
+
+    @Override
+    public String getConstraintTypeName() {
+        return typeName;
+    }
+}

+ 146 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/BezierCurve.java

@@ -0,0 +1,146 @@
+package com.jme3.scene.plugins.blender.curves;
+
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.blender.file.DynamicArray;
+import com.jme3.scene.plugins.blender.file.Structure;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that helps to calculate the bezier curves calues. It uses doubles for performing calculations to minimize
+ * floating point operations errors.
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class BezierCurve {
+
+    public static final int X_VALUE = 0;
+    public static final int Y_VALUE = 1;
+    public static final int Z_VALUE = 2;
+    /**
+     * The type of the curve. Describes the data it modifies.
+     * Used in ipos calculations.
+     */
+    private int             type;
+    /** The dimension of the curve. */
+    private int             dimension;
+    /** A table of the bezier points. */
+    private float[][][]     bezierPoints;
+    /** Array that stores a radius for each bezier triple. */
+    private float[]         radiuses;
+
+    @SuppressWarnings("unchecked")
+    public BezierCurve(final int type, final List<Structure> bezTriples, final int dimension) {
+        if (dimension != 2 && dimension != 3) {
+            throw new IllegalArgumentException("The dimension of the curve should be 2 or 3!");
+        }
+        this.type = type;
+        this.dimension = dimension;
+        // first index of the bezierPoints table has the length of triples amount
+        // the second index points to a table od three points of a bezier triple (handle, point, handle)
+        // the third index specifies the coordinates of the specific point in a bezier triple
+        bezierPoints = new float[bezTriples.size()][3][dimension];
+        radiuses = new float[bezTriples.size()];
+        int i = 0, j, k;
+        for (Structure bezTriple : bezTriples) {
+            DynamicArray<Number> vec = (DynamicArray<Number>) bezTriple.getFieldValue("vec");
+            for (j = 0; j < 3; ++j) {
+                for (k = 0; k < dimension; ++k) {
+                    bezierPoints[i][j][k] = vec.get(j, k).floatValue();
+                }
+            }
+            radiuses[i++] = ((Number) bezTriple.getFieldValue("radius")).floatValue();
+        }
+    }
+
+    /**
+     * This method evaluates the data for the specified frame. The Y value is returned.
+     * @param frame
+     *            the frame for which the value is being calculated
+     * @param valuePart
+     *            this param specifies wheather we should return the X, Y or Z part of the result value; it should have
+     *            one of the following values: X_VALUE - the X factor of the result Y_VALUE - the Y factor of the result
+     *            Z_VALUE - the Z factor of the result
+     * @return the value of the curve
+     */
+    public float evaluate(int frame, int valuePart) {
+        for (int i = 0; i < bezierPoints.length - 1; ++i) {
+            if (frame >= bezierPoints[i][1][0] && frame <= bezierPoints[i + 1][1][0]) {
+                float t = (frame - bezierPoints[i][1][0]) / (bezierPoints[i + 1][1][0] - bezierPoints[i][1][0]);
+                float oneMinusT = 1.0f - t;
+                float oneMinusT2 = oneMinusT * oneMinusT;
+                float t2 = t * t;
+                return bezierPoints[i][1][valuePart] * oneMinusT2 * oneMinusT + 3.0f * bezierPoints[i][2][valuePart] * t * oneMinusT2 + 3.0f * bezierPoints[i + 1][0][valuePart] * t2 * oneMinusT + bezierPoints[i + 1][1][valuePart] * t2 * t;
+            }
+        }
+        if (frame < bezierPoints[0][1][0]) {
+            return bezierPoints[0][1][1];
+        } else { // frame>bezierPoints[bezierPoints.length-1][1][0]
+            return bezierPoints[bezierPoints.length - 1][1][1];
+        }
+    }
+
+    /**
+     * This method returns the frame where last bezier triple center point of the bezier curve is located.
+     * @return the frame number of the last defined bezier triple point for the curve
+     */
+    public int getLastFrame() {
+        return (int) bezierPoints[bezierPoints.length - 1][1][0];
+    }
+
+    /**
+     * This method returns the type of the bezier curve. The type describes the parameter that this curve modifies
+     * (ie. LocationX or rotationW of the feature).
+     * @return the type of the bezier curve
+     */
+    public int getType() {
+        return type;
+    }
+
+    /**
+     * The method returns the radius for the required bezier triple.
+     * 
+     * @param bezierTripleIndex
+     *            index of the bezier triple
+     * @return radius of the required bezier triple
+     */
+    public float getRadius(int bezierTripleIndex) {
+        return radiuses[bezierTripleIndex];
+    }
+
+    /**
+     * This method returns a list of control points for this curve.
+     * @return a list of control points for this curve.
+     */
+    public List<Vector3f> getControlPoints() {
+        List<Vector3f> controlPoints = new ArrayList<Vector3f>(bezierPoints.length * 3);
+        for (int i = 0; i < bezierPoints.length; ++i) {
+            controlPoints.add(new Vector3f(bezierPoints[i][0][0], bezierPoints[i][0][1], bezierPoints[i][0][2]));
+            controlPoints.add(new Vector3f(bezierPoints[i][1][0], bezierPoints[i][1][1], bezierPoints[i][1][2]));
+            controlPoints.add(new Vector3f(bezierPoints[i][2][0], bezierPoints[i][2][1], bezierPoints[i][2][2]));
+        }
+        return controlPoints;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("Bezier curve: ").append(type).append('\n');
+        for (int i = 0; i < bezierPoints.length; ++i) {
+            sb.append(this.toStringBezTriple(i)).append('\n');
+        }
+        return sb.toString();
+    }
+
+    /**
+     * This method converts the bezier triple of a specified index into text.
+     * @param tripleIndex
+     *            index of the triple
+     * @return text representation of the triple
+     */
+    private String toStringBezTriple(int tripleIndex) {
+        if (this.dimension == 2) {
+            return "[(" + bezierPoints[tripleIndex][0][0] + ", " + bezierPoints[tripleIndex][0][1] + ") (" + bezierPoints[tripleIndex][1][0] + ", " + bezierPoints[tripleIndex][1][1] + ") (" + bezierPoints[tripleIndex][2][0] + ", " + bezierPoints[tripleIndex][2][1] + ")]";
+        } else {
+            return "[(" + bezierPoints[tripleIndex][0][0] + ", " + bezierPoints[tripleIndex][0][1] + ", " + bezierPoints[tripleIndex][0][2] + ") (" + bezierPoints[tripleIndex][1][0] + ", " + bezierPoints[tripleIndex][1][1] + ", " + bezierPoints[tripleIndex][1][2] + ") (" + bezierPoints[tripleIndex][2][0] + ", " + bezierPoints[tripleIndex][2][1] + ", " + bezierPoints[tripleIndex][2][2] + ")]";
+        }
+    }
+}

+ 838 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/CurvesHelper.java

@@ -0,0 +1,838 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.curves;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.logging.Logger;
+
+import com.jme3.material.Material;
+import com.jme3.material.RenderState.FaceCullMode;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Spline;
+import com.jme3.math.Spline.SplineType;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.BlenderInputStream;
+import com.jme3.scene.plugins.blender.file.DynamicArray;
+import com.jme3.scene.plugins.blender.file.FileBlockHeader;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.materials.MaterialContext;
+import com.jme3.scene.plugins.blender.materials.MaterialHelper;
+import com.jme3.scene.plugins.blender.objects.Properties;
+import com.jme3.scene.shape.Curve;
+import com.jme3.scene.shape.Surface;
+import com.jme3.util.BufferUtils;
+
+/**
+ * A class that is used in mesh calculations.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class CurvesHelper extends AbstractBlenderHelper {
+    private static final Logger LOGGER                      = Logger.getLogger(CurvesHelper.class.getName());
+
+    /** Minimum basis U function degree for NURBS curves and surfaces. */
+    protected int               minimumBasisUFunctionDegree = 4;
+    /** Minimum basis V function degree for NURBS curves and surfaces. */
+    protected int               minimumBasisVFunctionDegree = 4;
+
+    /**
+     * This constructor parses the given blender version and stores the result. Some functionalities may differ in
+     * different blender versions.
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public CurvesHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    /**
+     * This method converts given curve structure into a list of geometries representing the curve. The list is used here because on object
+     * can have several separate curves.
+     * @param curveStructure
+     *            the curve structure
+     * @param blenderContext
+     *            the blender context
+     * @return a list of geometries repreenting a single curve object
+     * @throws BlenderFileException
+     */
+    public List<Geometry> toCurve(Structure curveStructure, BlenderContext blenderContext) throws BlenderFileException {
+        String name = curveStructure.getName();
+        int flag = ((Number) curveStructure.getFieldValue("flag")).intValue();
+        boolean is3D = (flag & 0x01) != 0;
+        boolean isFront = (flag & 0x02) != 0 && !is3D;
+        boolean isBack = (flag & 0x04) != 0 && !is3D;
+        if (isFront) {
+            LOGGER.warning("No front face in curve implemented yet!");// TODO: implement front face
+        }
+        if (isBack) {
+            LOGGER.warning("No back face in curve implemented yet!");// TODO: implement back face
+        }
+
+        // reading nurbs (and sorting them by material)
+        List<Structure> nurbStructures = ((Structure) curveStructure.getFieldValue("nurb")).evaluateListBase();
+        Map<Number, List<Structure>> nurbs = new HashMap<Number, List<Structure>>();
+        for (Structure nurb : nurbStructures) {
+            Number matNumber = (Number) nurb.getFieldValue("mat_nr");
+            List<Structure> nurbList = nurbs.get(matNumber);
+            if (nurbList == null) {
+                nurbList = new ArrayList<Structure>();
+                nurbs.put(matNumber, nurbList);
+            }
+            nurbList.add(nurb);
+        }
+
+        // getting materials
+        MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
+        MaterialContext[] materialContexts = materialHelper.getMaterials(curveStructure, blenderContext);
+        Material defaultMaterial = null;
+        if (materialContexts != null) {
+            for (MaterialContext materialContext : materialContexts) {
+                materialContext.setFaceCullMode(FaceCullMode.Off);
+            }
+        } else {
+            defaultMaterial = blenderContext.getDefaultMaterial().clone();
+            defaultMaterial.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
+        }
+
+        // getting or creating bevel object
+        List<Geometry> bevelObject = null;
+        Pointer pBevelObject = (Pointer) curveStructure.getFieldValue("bevobj");
+        if (pBevelObject.isNotNull()) {
+            Pointer pBevelStructure = (Pointer) pBevelObject.fetchData().get(0).getFieldValue("data");
+            Structure bevelStructure = pBevelStructure.fetchData().get(0);
+            bevelObject = this.toCurve(bevelStructure, blenderContext);
+        } else {
+            int bevResol = ((Number) curveStructure.getFieldValue("bevresol")).intValue();
+            float extrude = ((Number) curveStructure.getFieldValue("ext1")).floatValue();
+            float bevelDepth = ((Number) curveStructure.getFieldValue("ext2")).floatValue();
+            if (bevelDepth > 0.0f) {
+                float handlerLength = bevelDepth / 2.0f;
+
+                List<Vector3f> conrtolPoints = new ArrayList<Vector3f>(extrude > 0.0f ? 19 : 13);
+                if (extrude > 0.0f) {
+                    conrtolPoints.add(new Vector3f(-bevelDepth, 0, extrude));
+                    conrtolPoints.add(new Vector3f(-bevelDepth, 0, -handlerLength + extrude));
+                    conrtolPoints.add(new Vector3f(-bevelDepth, 0, handlerLength - extrude));
+                }
+
+                conrtolPoints.add(new Vector3f(-bevelDepth, 0, -extrude));
+                conrtolPoints.add(new Vector3f(-bevelDepth, 0, -handlerLength - extrude));
+
+                conrtolPoints.add(new Vector3f(-handlerLength, 0, -bevelDepth - extrude));
+                conrtolPoints.add(new Vector3f(0, 0, -bevelDepth - extrude));
+                conrtolPoints.add(new Vector3f(handlerLength, 0, -bevelDepth - extrude));
+
+                if (extrude > 0.0f) {
+                    conrtolPoints.add(new Vector3f(bevelDepth, 0, -extrude - handlerLength));
+                    conrtolPoints.add(new Vector3f(bevelDepth, 0, -extrude));
+                    conrtolPoints.add(new Vector3f(bevelDepth, 0, -extrude + handlerLength));
+                }
+
+                conrtolPoints.add(new Vector3f(bevelDepth, 0, extrude - handlerLength));
+                conrtolPoints.add(new Vector3f(bevelDepth, 0, extrude));
+                conrtolPoints.add(new Vector3f(bevelDepth, 0, extrude + handlerLength));
+
+                conrtolPoints.add(new Vector3f(handlerLength, 0, bevelDepth + extrude));
+                conrtolPoints.add(new Vector3f(0, 0, bevelDepth + extrude));
+                conrtolPoints.add(new Vector3f(-handlerLength, 0, bevelDepth + extrude));
+
+                conrtolPoints.add(new Vector3f(-bevelDepth, 0, handlerLength + extrude));
+                conrtolPoints.add(new Vector3f(-bevelDepth, 0, extrude));
+
+                Spline bevelSpline = new Spline(SplineType.Bezier, conrtolPoints, 0, false);
+                Curve bevelCurve = new Curve(bevelSpline, bevResol);
+                bevelObject = new ArrayList<Geometry>(1);
+                bevelObject.add(new Geometry("", bevelCurve));
+            } else if (extrude > 0.0f) {
+                Spline bevelSpline = new Spline(SplineType.Linear, new Vector3f[] { new Vector3f(0, 0, -extrude), new Vector3f(0, 0, extrude) }, 1, false);
+                Curve bevelCurve = new Curve(bevelSpline, bevResol);
+                bevelObject = new ArrayList<Geometry>(1);
+                bevelObject.add(new Geometry("", bevelCurve));
+            }
+        }
+
+        // getting taper object
+        Spline taperObject = null;
+        Pointer pTaperObject = (Pointer) curveStructure.getFieldValue("taperobj");
+        if (bevelObject != null && pTaperObject.isNotNull()) {
+            Pointer pTaperStructure = (Pointer) pTaperObject.fetchData().get(0).getFieldValue("data");
+            Structure taperStructure = pTaperStructure.fetchData().get(0);
+            taperObject = this.loadTaperObject(taperStructure);
+        }
+
+        Vector3f loc = this.getLoc(curveStructure);
+        // creating the result curves
+        List<Geometry> result = new ArrayList<Geometry>(nurbs.size());
+        for (Entry<Number, List<Structure>> nurbEntry : nurbs.entrySet()) {
+            for (Structure nurb : nurbEntry.getValue()) {
+                int type = ((Number) nurb.getFieldValue("type")).intValue();
+                List<Geometry> nurbGeoms = null;
+                if ((type & 0x01) != 0) {// Bezier curve
+                    nurbGeoms = this.loadBezierCurve(loc, nurb, bevelObject, taperObject, blenderContext);
+                } else if ((type & 0x04) != 0) {// NURBS
+                    nurbGeoms = this.loadNurb(loc, nurb, bevelObject, taperObject, blenderContext);
+                }
+                if (nurbGeoms != null) {// setting the name and assigning materials
+                    for (Geometry nurbGeom : nurbGeoms) {
+                        if (materialContexts != null) {
+                            materialContexts[nurbEntry.getKey().intValue()].applyMaterial(nurbGeom, curveStructure.getOldMemoryAddress(), null, blenderContext);
+                        } else {
+                            nurbGeom.setMaterial(defaultMaterial);
+                        }
+                        nurbGeom.setName(name);
+                        result.add(nurbGeom);
+                    }
+                }
+            }
+        }
+
+        // reading custom properties
+        if (blenderContext.getBlenderKey().isLoadObjectProperties() && result.size() > 0) {
+            Properties properties = this.loadProperties(curveStructure, blenderContext);
+            // the loaded property is a group property, so we need to get each value and set it to every geometry of the curve
+            if (properties != null && properties.getValue() != null) {
+                for(Geometry geom : result) {
+                    this.applyProperties(geom, properties);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * This method loads the bezier curve.
+     * @param loc
+     *            the translation of the curve
+     * @param nurb
+     *            the nurb structure
+     * @param bevelObject
+     *            the bevel object
+     * @param taperObject
+     *            the taper object
+     * @param blenderContext
+     *            the blender context
+     * @return a list of geometries representing the curves
+     * @throws BlenderFileException
+     *             an exception is thrown when there are problems with the blender file
+     */
+    protected List<Geometry> loadBezierCurve(Vector3f loc, Structure nurb, List<Geometry> bevelObject, Spline taperObject, BlenderContext blenderContext) throws BlenderFileException {
+        Pointer pBezierTriple = (Pointer) nurb.getFieldValue("bezt");
+        List<Geometry> result = new ArrayList<Geometry>();
+        if (pBezierTriple.isNotNull()) {
+            boolean smooth = (((Number) nurb.getFlatFieldValue("flag")).intValue() & 0x01) != 0;
+            int resolution = ((Number) nurb.getFieldValue("resolu")).intValue();
+            boolean cyclic = (((Number) nurb.getFieldValue("flagu")).intValue() & 0x01) != 0;
+
+            // creating the curve object
+            BezierCurve bezierCurve = new BezierCurve(0, pBezierTriple.fetchData(), 3);
+            List<Vector3f> controlPoints = bezierCurve.getControlPoints();
+            if (fixUpAxis) {
+                for (Vector3f v : controlPoints) {
+                    float y = v.y;
+                    v.y = v.z;
+                    v.z = -y;
+                }
+            }
+
+            if (bevelObject != null && taperObject == null) {// create taper object using the scales of the bezier triple
+                int triplesCount = controlPoints.size() / 3;
+                List<Vector3f> taperControlPoints = new ArrayList<Vector3f>(triplesCount);
+                for (int i = 0; i < triplesCount; ++i) {
+                    taperControlPoints.add(new Vector3f(controlPoints.get(i * 3 + 1).x, bezierCurve.getRadius(i), 0));
+                }
+                taperObject = new Spline(SplineType.Linear, taperControlPoints, 0, false);
+            }
+
+            if (cyclic) {
+                // copy the first three points at the end
+                for (int i = 0; i < 3; ++i) {
+                    controlPoints.add(controlPoints.get(i));
+                }
+            }
+            // removing the first and last handles
+            controlPoints.remove(0);
+            controlPoints.remove(controlPoints.size() - 1);
+
+            // creating curve
+            Spline spline = new Spline(SplineType.Bezier, controlPoints, 0, false);
+            Curve curve = new Curve(spline, resolution);
+            if (bevelObject == null) {// creating a normal curve
+                Geometry curveGeometry = new Geometry(null, curve);
+                result.add(curveGeometry);
+                // TODO: use front and back flags; surface excluding algorithm for bezier circles should be added
+            } else {// creating curve with bevel and taper shape
+                result = this.applyBevelAndTaper(curve, bevelObject, taperObject, smooth, blenderContext);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * This method loads the NURBS curve or surface.
+     * @param loc
+     *            object's location
+     * @param nurb
+     *            the NURBS data structure
+     * @param bevelObject
+     *            the bevel object to be applied
+     * @param taperObject
+     *            the taper object to be applied
+     * @param blenderContext
+     *            the blender context
+     * @return a list of geometries that represents the loaded NURBS curve or surface
+     * @throws BlenderFileException
+     *             an exception is throw when problems with blender loaded data occurs
+     */
+    @SuppressWarnings("unchecked")
+    protected List<Geometry> loadNurb(Vector3f loc, Structure nurb, List<Geometry> bevelObject, Spline taperObject, BlenderContext blenderContext) throws BlenderFileException {
+        // loading the knots
+        List<Float>[] knots = new List[2];
+        Pointer[] pKnots = new Pointer[] { (Pointer) nurb.getFieldValue("knotsu"), (Pointer) nurb.getFieldValue("knotsv") };
+        for (int i = 0; i < knots.length; ++i) {
+            if (pKnots[i].isNotNull()) {
+                FileBlockHeader fileBlockHeader = blenderContext.getFileBlock(pKnots[i].getOldMemoryAddress());
+                BlenderInputStream blenderInputStream = blenderContext.getInputStream();
+                blenderInputStream.setPosition(fileBlockHeader.getBlockPosition());
+                int knotsAmount = fileBlockHeader.getCount() * fileBlockHeader.getSize() / 4;
+                knots[i] = new ArrayList<Float>(knotsAmount);
+                for (int j = 0; j < knotsAmount; ++j) {
+                    knots[i].add(Float.valueOf(blenderInputStream.readFloat()));
+                }
+            }
+        }
+
+        // loading the flags and orders (basis functions degrees)
+        int flagU = ((Number) nurb.getFieldValue("flagu")).intValue();
+        int flagV = ((Number) nurb.getFieldValue("flagv")).intValue();
+        int orderU = ((Number) nurb.getFieldValue("orderu")).intValue();
+        int orderV = ((Number) nurb.getFieldValue("orderv")).intValue();
+
+        // loading control points and their weights
+        int pntsU = ((Number) nurb.getFieldValue("pntsu")).intValue();
+        int pntsV = ((Number) nurb.getFieldValue("pntsv")).intValue();
+        List<Structure> bPoints = ((Pointer) nurb.getFieldValue("bp")).fetchData();
+        List<List<Vector4f>> controlPoints = new ArrayList<List<Vector4f>>(pntsV);
+        for (int i = 0; i < pntsV; ++i) {
+            List<Vector4f> uControlPoints = new ArrayList<Vector4f>(pntsU);
+            for (int j = 0; j < pntsU; ++j) {
+                DynamicArray<Float> vec = (DynamicArray<Float>) bPoints.get(j + i * pntsU).getFieldValue("vec");
+                if (fixUpAxis) {
+                    uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(2).floatValue(), -vec.get(1).floatValue(), vec.get(3).floatValue()));
+                } else {
+                    uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(1).floatValue(), vec.get(2).floatValue(), vec.get(3).floatValue()));
+                }
+            }
+            if ((flagU & 0x01) != 0) {
+                for (int k = 0; k < orderU - 1; ++k) {
+                    uControlPoints.add(uControlPoints.get(k));
+                }
+            }
+            controlPoints.add(uControlPoints);
+        }
+        if ((flagV & 0x01) != 0) {
+            for (int k = 0; k < orderV - 1; ++k) {
+                controlPoints.add(controlPoints.get(k));
+            }
+        }
+
+        int resolu = ((Number) nurb.getFieldValue("resolu")).intValue() + 1;
+        List<Geometry> result;
+        if (knots[1] == null) {// creating the curve
+            Spline nurbSpline = new Spline(controlPoints.get(0), knots[0]);
+            Curve nurbCurve = new Curve(nurbSpline, resolu);
+            if (bevelObject != null) {
+                result = this.applyBevelAndTaper(nurbCurve, bevelObject, taperObject, true, blenderContext);// TODO: smooth
+            } else {
+                result = new ArrayList<Geometry>(1);
+                Geometry nurbGeometry = new Geometry("", nurbCurve);
+                result.add(nurbGeometry);
+            }
+        } else {// creating the nurb surface
+            int resolv = ((Number) nurb.getFieldValue("resolv")).intValue() + 1;
+            Surface nurbSurface = Surface.createNurbsSurface(controlPoints, knots, resolu, resolv, orderU, orderV);
+            Geometry nurbGeometry = new Geometry("", nurbSurface);
+            result = new ArrayList<Geometry>(1);
+            result.add(nurbGeometry);
+        }
+        return result;
+    }
+
+    /**
+     * The method computes the taper scale on the given point on the curve.
+     * 
+     * @param taper
+     *            the taper object that defines the scale
+     * @param percent
+     *            the percent of the 'road' along the curve
+     * @return scale on the pointed place along the curve
+     */
+    protected float getTaperScale(Spline taper, float percent) {
+        if (taper == null) {
+            return 1;// return scale = 1 if no taper is applied
+        }
+        percent = FastMath.clamp(percent, 0, 1);
+        List<Float> segmentLengths = taper.getSegmentsLength();
+        float percentLength = taper.getTotalLength() * percent;
+        float partLength = 0;
+        int i;
+        for (i = 0; i < segmentLengths.size(); ++i) {
+            partLength += segmentLengths.get(i);
+            if (partLength > percentLength) {
+                partLength -= segmentLengths.get(i);
+                percentLength -= partLength;
+                percent = percentLength / segmentLengths.get(i);
+                break;
+            }
+        }
+        // do not cross the line :)
+        if (percent >= 1) {
+            percent = 1;
+            --i;
+        }
+        if (taper.getType() == SplineType.Bezier) {
+            i *= 3;
+        }
+        return taper.interpolate(percent, i, null).y;
+    }
+
+    /**
+     * This method applies bevel and taper objects to the curve.
+     * @param curve
+     *            the curve we apply the objects to
+     * @param bevelObject
+     *            the bevel object
+     * @param taperObject
+     *            the taper object
+     * @param smooth
+     *            the smooth flag
+     * @param blenderContext
+     *            the blender context
+     * @return a list of geometries representing the beveled and/or tapered curve
+     */
+    protected List<Geometry> applyBevelAndTaper(Curve curve, List<Geometry> bevelObject, Spline taperObject, boolean smooth, BlenderContext blenderContext) {
+        Vector3f[] curvePoints = BufferUtils.getVector3Array(curve.getFloatBuffer(Type.Position));
+        Vector3f subtractResult = new Vector3f();
+        float curveLength = curve.getLength();
+
+        FloatBuffer[] vertexBuffers = new FloatBuffer[bevelObject.size()];
+        FloatBuffer[] normalBuffers = new FloatBuffer[bevelObject.size()];
+        IndexBuffer[] indexBuffers = new IndexBuffer[bevelObject.size()];
+        for (int geomIndex = 0; geomIndex < bevelObject.size(); ++geomIndex) {
+            Mesh mesh = bevelObject.get(geomIndex).getMesh();
+            Vector3f[] positions = BufferUtils.getVector3Array(mesh.getFloatBuffer(Type.Position));
+            Vector3f[] bevelPoints = this.transformToFirstLineOfBevelPoints(positions, curvePoints[0], curvePoints[1]);
+
+            List<Vector3f[]> bevels = new ArrayList<Vector3f[]>(curvePoints.length);
+            bevels.add(bevelPoints);
+
+            vertexBuffers[geomIndex] = BufferUtils.createFloatBuffer(bevelPoints.length * 3 * curvePoints.length * (smooth ? 1 : 6));
+            for (int i = 1; i < curvePoints.length - 1; ++i) {
+                bevelPoints = this.transformBevel(bevelPoints, curvePoints[i - 1], curvePoints[i], curvePoints[i + 1]);
+                bevels.add(bevelPoints);
+            }
+            bevelPoints = this.transformBevel(bevelPoints, curvePoints[curvePoints.length - 2], curvePoints[curvePoints.length - 1], null);
+            bevels.add(bevelPoints);
+
+            if (bevels.size() > 2) {
+                // changing the first and last bevel so that they are parallel to their neighbours (blender works this way)
+                // notice this implicates that the distances of every corresponding point in th two bevels must be identical and
+                // equal to the distance between the points on curve that define the bevel position
+                // so instead doing complicated rotations on each point we will simply properly translate each of them
+
+                int[][] pointIndexes = new int[][] { { 0, 1 }, { curvePoints.length - 1, curvePoints.length - 2 } };
+                for (int[] indexes : pointIndexes) {
+                    float distance = curvePoints[indexes[1]].subtract(curvePoints[indexes[0]], subtractResult).length();
+                    Vector3f[] bevel = bevels.get(indexes[0]);
+                    Vector3f[] nextBevel = bevels.get(indexes[1]);
+                    for (int i = 0; i < bevel.length; ++i) {
+                        float d = bevel[i].subtract(nextBevel[i], subtractResult).length();
+                        subtractResult.normalizeLocal().multLocal(distance - d);
+                        bevel[i].addLocal(subtractResult);
+                    }
+                }
+            }
+
+            // apply scales to the bevels
+            float lengthAlongCurve = 0;
+            for (int i = 0; i < curvePoints.length; ++i) {
+                if (i > 0) {
+                    lengthAlongCurve += curvePoints[i].subtract(curvePoints[i - 1], subtractResult).length();
+                }
+                float taperScale = this.getTaperScale(taperObject, i == 0 ? 0 : lengthAlongCurve / curveLength);
+                this.applyScale(bevels.get(i), curvePoints[i], taperScale);
+            }
+
+            if (smooth) {// add everything to the buffer
+                for (Vector3f[] bevel : bevels) {
+                    for (Vector3f d : bevel) {
+                        vertexBuffers[geomIndex].put(d.x);
+                        vertexBuffers[geomIndex].put(d.y);
+                        vertexBuffers[geomIndex].put(d.z);
+                    }
+                }
+            } else {// add vertices to the buffer duplicating them so that every vertex belongs only to a single triangle
+                for (int i = 0; i < curvePoints.length - 1; ++i) {
+                    for (int j = 0; j < bevelPoints.length - 1; ++j) {
+                        // first triangle
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j].x);
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j].y);
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j].z);
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].x);
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].y);
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].z);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].x);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].y);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].z);
+
+                        // second triangle
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].x);
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].y);
+                        vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].z);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j + 1].x);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j + 1].y);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j + 1].z);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].x);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].y);
+                        vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].z);
+                    }
+                }
+            }
+
+            indexBuffers[geomIndex] = this.generateIndexes(bevelPoints.length, curvePoints.length, smooth);
+            normalBuffers[geomIndex] = this.generateNormals(indexBuffers[geomIndex], vertexBuffers[geomIndex], smooth);
+        }
+
+        // creating and returning the result
+        List<Geometry> result = new ArrayList<Geometry>(vertexBuffers.length);
+        Float oneReferenceToCurveLength = new Float(curveLength);// its important for array modifier to use one reference here
+        for (int i = 0; i < vertexBuffers.length; ++i) {
+            Mesh mesh = new Mesh();
+            mesh.setBuffer(Type.Position, 3, vertexBuffers[i]);
+            if (indexBuffers[i].getBuffer() instanceof IntBuffer) {
+                mesh.setBuffer(Type.Index, 3, (IntBuffer) indexBuffers[i].getBuffer());
+            } else {
+                mesh.setBuffer(Type.Index, 3, (ShortBuffer) indexBuffers[i].getBuffer());
+            }
+            mesh.setBuffer(Type.Normal, 3, normalBuffers[i]);
+            Geometry g = new Geometry("g" + i, mesh);
+            g.setUserData("curveLength", oneReferenceToCurveLength);
+            g.updateModelBound();
+            result.add(g);
+        }
+        return result;
+    }
+
+    /**
+     * the method applies scale for the given bevel points. The points table is
+     * being modified so expect ypur result there.
+     * 
+     * @param points
+     *            the bevel points
+     * @param centerPoint
+     *            the center point of the bevel
+     * @param scale
+     *            the scale to be applied
+     */
+    private void applyScale(Vector3f[] points, Vector3f centerPoint, float scale) {
+        Vector3f taperScaleVector = new Vector3f();
+        for (Vector3f p : points) {
+            taperScaleVector.set(centerPoint).subtractLocal(p).multLocal(1 - scale);
+            p.addLocal(taperScaleVector);
+        }
+    }
+
+    /**
+     * The method generates normal buffer for the created mesh of the curve.
+     * 
+     * @param indexes
+     *            the indexes of the mesh points
+     * @param points
+     *            the mesh's points
+     * @param smooth
+     *            the flag indicating if the result is to be smooth or solid
+     * @return normals buffer for the mesh
+     */
+    private FloatBuffer generateNormals(IndexBuffer indexes, FloatBuffer points, boolean smooth) {
+        Map<Integer, Vector3f> normalMap = new TreeMap<Integer, Vector3f>();
+        Vector3f[] allVerts = BufferUtils.getVector3Array(points);
+
+        for (int i = 0; i < indexes.size(); i += 3) {
+            int index1 = indexes.get(i);
+            int index2 = indexes.get(i + 1);
+            int index3 = indexes.get(i + 2);
+
+            Vector3f n = FastMath.computeNormal(allVerts[index1], allVerts[index2], allVerts[index3]);
+            this.addNormal(n, normalMap, smooth, index1, index2, index3);
+        }
+
+        FloatBuffer normals = BufferUtils.createFloatBuffer(normalMap.size() * 3);
+        for (Entry<Integer, Vector3f> entry : normalMap.entrySet()) {
+            normals.put(entry.getValue().x);
+            normals.put(entry.getValue().y);
+            normals.put(entry.getValue().z);
+        }
+        return normals;
+    }
+
+    /**
+     * The amount of faces in the final mesh is the amount of edges in the bevel
+     * curve (which is less by 1 than its number of vertices) multiplied by 2
+     * (because each edge has two faces assigned on both sides) and multiplied
+     * by the amount of bevel curve repeats which is equal to the amount of
+     * vertices on the target curve finally we need to subtract the bevel edges
+     * amount 2 times because the border edges have only one face attached and
+     * at last multiply everything by 3 because each face needs 3 indexes to be
+     * described
+     * 
+     * @param bevelShapeVertexCount
+     *            amount of points in bevel shape
+     * @param bevelRepeats
+     *            amount of bevel shapes along the curve
+     * @param smooth
+     *            the smooth flag
+     * @return index buffer for the mesh
+     */
+    private IndexBuffer generateIndexes(int bevelShapeVertexCount, int bevelRepeats, boolean smooth) {
+        int putIndex = 0;
+        if (smooth) {
+            int indexBufferSize = (bevelRepeats - 1) * (bevelShapeVertexCount - 1) * 6;
+            IndexBuffer result = IndexBuffer.createIndexBuffer(indexBufferSize, indexBufferSize);
+
+            for (int i = 0; i < bevelRepeats - 1; ++i) {
+                for (int j = 0; j < bevelShapeVertexCount - 1; ++j) {
+                    result.put(putIndex++, i * bevelShapeVertexCount + j);
+                    result.put(putIndex++, i * bevelShapeVertexCount + j + 1);
+                    result.put(putIndex++, (i + 1) * bevelShapeVertexCount + j);
+
+                    result.put(putIndex++, i * bevelShapeVertexCount + j + 1);
+                    result.put(putIndex++, (i + 1) * bevelShapeVertexCount + j + 1);
+                    result.put(putIndex++, (i + 1) * bevelShapeVertexCount + j);
+                }
+            }
+            return result;
+        } else {
+            // every pair of bevel vertices belongs to two triangles
+            // we have the same amount of pairs as the amount of vertices in bevel
+            // so the amount of triangles is: bevelShapeVertexCount * 2 * (bevelRepeats - 1)
+            // and this gives the amount of vertices in non smooth shape as below ...
+            int indexBufferSize = bevelShapeVertexCount * bevelRepeats * 6;// 6 = 2 * 3 where 2 is stated above and 3 is the count of vertices for each triangle
+            IndexBuffer result = IndexBuffer.createIndexBuffer(indexBufferSize, indexBufferSize);
+            for (int i = 0; i < indexBufferSize; ++i) {
+                result.put(putIndex++, i);
+            }
+            return result;
+        }
+    }
+
+    /**
+     * The method transforms the bevel along the curve.
+     * 
+     * @param bevel
+     *            the bevel to be transformed
+     * @param prevPos
+     *            previous curve point
+     * @param currPos
+     *            current curve point (here the center of the new bevel will be
+     *            set)
+     * @param nextPos
+     *            next curve point
+     * @return points of transformed bevel
+     */
+    private Vector3f[] transformBevel(Vector3f[] bevel, Vector3f prevPos, Vector3f currPos, Vector3f nextPos) {
+        bevel = bevel.clone();
+
+        // currPos and directionVector define the line in 3D space
+        Vector3f directionVector = prevPos != null ? currPos.subtract(prevPos) : nextPos.subtract(currPos);
+        directionVector.normalizeLocal();
+
+        // plane is described by equation: Ax + By + Cz + D = 0 where planeNormal = [A, B, C] and D = -(Ax + By + Cz)
+        Vector3f planeNormal = null;
+        if (prevPos != null) {
+            planeNormal = currPos.subtract(prevPos).normalizeLocal();
+            if (nextPos != null) {
+                planeNormal.addLocal(nextPos.subtract(currPos).normalizeLocal()).normalizeLocal();
+            }
+        } else {
+            planeNormal = nextPos.subtract(currPos).normalizeLocal();
+        }
+        float D = -planeNormal.dot(currPos);// D = -(Ax + By + Cz)
+
+        // now we need to compute paralell cast of each bevel point on the plane, the leading line is already known
+        // parametric equation of a line: x = px + vx * t; y = py + vy * t; z = pz + vz * t
+        // where p = currPos and v = directionVector
+        // using x, y and z in plane equation we get value of 't' that will allow us to compute the point where plane and line cross
+        float temp = planeNormal.dot(directionVector);
+        for (int i = 0; i < bevel.length; ++i) {
+            float t = -(planeNormal.dot(bevel[i]) + D) / temp;
+            if (fixUpAxis) {
+                bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, bevel[i].y + directionVector.y * t, bevel[i].z + directionVector.z * t);
+            } else {
+                bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, -bevel[i].z + directionVector.z * t, bevel[i].y + directionVector.y * t);
+            }
+        }
+        return bevel;
+    }
+
+    /**
+     * This method transforms the first line of the bevel points positioning it
+     * on the first point of the curve.
+     * 
+     * @param startingLinePoints
+     *            the vbevel shape points
+     * @param firstCurvePoint
+     *            the first curve's point
+     * @param secondCurvePoint
+     *            the second curve's point
+     * @return points of transformed bevel
+     */
+    private Vector3f[] transformToFirstLineOfBevelPoints(Vector3f[] startingLinePoints, Vector3f firstCurvePoint, Vector3f secondCurvePoint) {
+        Vector3f planeNormal = secondCurvePoint.subtract(firstCurvePoint).normalizeLocal();
+
+        float angle = FastMath.acos(planeNormal.dot(Vector3f.UNIT_Y));
+        planeNormal.crossLocal(Vector3f.UNIT_Y).normalizeLocal();// planeNormal is the rotation axis now
+        Quaternion pointRotation = new Quaternion();
+        pointRotation.fromAngleAxis(angle, planeNormal);
+
+        Matrix4f m = new Matrix4f();
+        m.setRotationQuaternion(pointRotation);
+        m.setTranslation(firstCurvePoint);
+
+        float[] temp = new float[] { 0, 0, 0, 1 };
+        Vector3f[] verts = new Vector3f[startingLinePoints.length];
+        for (int j = 0; j < verts.length; ++j) {
+            temp[0] = startingLinePoints[j].x;
+            temp[1] = startingLinePoints[j].y;
+            temp[2] = startingLinePoints[j].z;
+            temp = m.mult(temp);// the result is stored in the array
+            if (fixUpAxis) {
+                verts[j] = new Vector3f(temp[0], -temp[2], temp[1]);
+            } else {
+                verts[j] = new Vector3f(temp[0], temp[1], temp[2]);
+            }
+        }
+        return verts;
+    }
+
+    /**
+     * The method adds a normal to the given map. Depending in the smooth factor
+     * it is either merged with the revious normal or not.
+     * 
+     * @param normalToAdd
+     *            the normal vector to be added
+     * @param normalMap
+     *            the normal map where we add vectors
+     * @param smooth
+     *            the smooth flag
+     * @param indexes
+     *            the indexes of the normals
+     */
+    private void addNormal(Vector3f normalToAdd, Map<Integer, Vector3f> normalMap, boolean smooth, int... indexes) {
+        for (int index : indexes) {
+            Vector3f n = normalMap.get(index);
+            if (!smooth || n == null) {
+                normalMap.put(index, normalToAdd.clone());
+            } else {
+                n.addLocal(normalToAdd).normalizeLocal();
+            }
+        }
+    }
+
+    /**
+     * This method loads the taper object.
+     * 
+     * @param taperStructure
+     *            the taper structure
+     * @return the taper object
+     * @throws BlenderFileException
+     */
+    protected Spline loadTaperObject(Structure taperStructure) throws BlenderFileException {
+        // reading nurbs
+        List<Structure> nurbStructures = ((Structure) taperStructure.getFieldValue("nurb")).evaluateListBase();
+        for (Structure nurb : nurbStructures) {
+            Pointer pBezierTriple = (Pointer) nurb.getFieldValue("bezt");
+            if (pBezierTriple.isNotNull()) {
+                // creating the curve object
+                BezierCurve bezierCurve = new BezierCurve(0, pBezierTriple.fetchData(), 3);
+                List<Vector3f> controlPoints = bezierCurve.getControlPoints();
+                // removing the first and last handles
+                controlPoints.remove(0);
+                controlPoints.remove(controlPoints.size() - 1);
+
+                // return the first taper curve that has more than 3 control points
+                if (controlPoints.size() > 3) {
+                    return new Spline(SplineType.Bezier, controlPoints, 0, false);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * This method returns the translation of the curve. The UP axis is taken
+     * into account here.
+     * 
+     * @param curveStructure
+     *            the curve structure
+     * @return curve translation
+     */
+    @SuppressWarnings("unchecked")
+    protected Vector3f getLoc(Structure curveStructure) {
+        DynamicArray<Number> locArray = (DynamicArray<Number>) curveStructure.getFieldValue("loc");
+        if (fixUpAxis) {
+            return new Vector3f(locArray.get(0).floatValue(), locArray.get(1).floatValue(), -locArray.get(2).floatValue());
+        } else {
+            return new Vector3f(locArray.get(0).floatValue(), locArray.get(2).floatValue(), locArray.get(1).floatValue());
+        }
+    }
+}

+ 77 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderFileException.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.file;
+
+/**
+ * This exception is thrown when blend file data is somehow invalid.
+ * @author Marcin Roguski
+ */
+public class BlenderFileException extends Exception {
+
+    private static final long serialVersionUID = 7573482836437866767L;
+
+    /**
+     * Constructor. Creates an exception with no description.
+     */
+    public BlenderFileException() {
+        // this constructor has no message
+    }
+
+    /**
+     * Constructor. Creates an exception containing the given message.
+     * @param message
+     *            the message describing the problem that occured
+     */
+    public BlenderFileException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor. Creates an exception that is based upon other thrown object. It contains the whole stacktrace then.
+     * @param throwable
+     *            an exception/error that occured
+     */
+    public BlenderFileException(Throwable throwable) {
+        super(throwable);
+    }
+
+    /**
+     * Constructor. Creates an exception with both a message and stacktrace.
+     * @param message
+     *            the message describing the problem that occured
+     * @param throwable
+     *            an exception/error that occured
+     */
+    public BlenderFileException(String message, Throwable throwable) {
+        super(message, throwable);
+    }
+}

+ 371 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderInputStream.java

@@ -0,0 +1,371 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.file;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * An input stream with random access to data.
+ * @author Marcin Roguski
+ */
+public class BlenderInputStream extends InputStream {
+
+    private static final Logger LOGGER              = Logger.getLogger(BlenderInputStream.class.getName());
+    /** The default size of the blender buffer. */
+    private static final int    DEFAULT_BUFFER_SIZE = 1048576;                                             // 1MB
+    /**
+     * Size of a pointer; all pointers in the file are stored in this format. '_' means 4 bytes and '-' means 8 bytes.
+     */
+    private int                 pointerSize;
+    /**
+     * Type of byte ordering used; 'v' means little endian and 'V' means big endian.
+     */
+    private char                endianess;
+    /** Version of Blender the file was created in; '248' means version 2.48. */
+    private String              versionNumber;
+    /** The buffer we store the read data to. */
+    protected byte[]            cachedBuffer;
+    /** The total size of the stored data. */
+    protected int               size;
+    /** The current position of the read cursor. */
+    protected int               position;
+
+    /**
+     * Constructor. The input stream is stored and used to read data.
+     * @param inputStream
+     *            the stream we read data from
+     * @throws BlenderFileException
+     *             this exception is thrown if the file header has some invalid data
+     */
+    public BlenderInputStream(InputStream inputStream) throws BlenderFileException {
+        // the size value will canche while reading the file; the available() method cannot be counted on
+        try {
+            size = inputStream.available();
+        } catch (IOException e) {
+            size = 0;
+        }
+        if (size <= 0) {
+            size = BlenderInputStream.DEFAULT_BUFFER_SIZE;
+        }
+
+        // buffered input stream is used here for much faster file reading
+        BufferedInputStream bufferedInputStream;
+        if (inputStream instanceof BufferedInputStream) {
+            bufferedInputStream = (BufferedInputStream) inputStream;
+        } else {
+            bufferedInputStream = new BufferedInputStream(inputStream);
+        }
+
+        try {
+            this.readStreamToCache(bufferedInputStream);
+        } catch (IOException e) {
+            throw new BlenderFileException("Problems occured while caching the file!", e);
+        } finally {
+            try {
+                inputStream.close();
+            } catch (IOException e) {
+                LOGGER.warning("Unable to close stream with blender file.");
+            }
+        }
+
+        try {
+            this.readFileHeader();
+        } catch (BlenderFileException e) {// the file might be packed, don't panic, try one more time ;)
+            this.decompressFile();
+            position = 0;
+            this.readFileHeader();
+        }
+    }
+
+    /**
+     * This method reads the whole stream into a buffer.
+     * @param inputStream
+     *            the stream to read the file data from
+     * @throws IOException
+     *             an exception is thrown when data read from the stream is invalid or there are problems with i/o
+     *             operations
+     */
+    private void readStreamToCache(InputStream inputStream) throws IOException {
+        int data = inputStream.read();
+        cachedBuffer = new byte[size];
+        size = 0;// this will count the actual size
+        while (data != -1) {
+            if (size >= cachedBuffer.length) {// widen the cached array
+                byte[] newBuffer = new byte[cachedBuffer.length + (cachedBuffer.length >> 1)];
+                System.arraycopy(cachedBuffer, 0, newBuffer, 0, cachedBuffer.length);
+                cachedBuffer = newBuffer;
+            }
+            cachedBuffer[size++] = (byte) data;
+            data = inputStream.read();
+        }
+    }
+
+    /**
+     * This method is used when the blender file is gzipped. It decompresses the data and stores it back into the
+     * cachedBuffer field.
+     */
+    private void decompressFile() {
+        GZIPInputStream gis = null;
+        try {
+            gis = new GZIPInputStream(new ByteArrayInputStream(cachedBuffer));
+            this.readStreamToCache(gis);
+        } catch (IOException e) {
+            throw new IllegalStateException("IO errors occured where they should NOT! " + "The data is already buffered at this point!", e);
+        } finally {
+            try {
+                if (gis != null) {
+                    gis.close();
+                }
+            } catch (IOException e) {
+                LOGGER.warning(e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * This method loads the header from the given stream during instance creation.
+     * @param inputStream
+     *            the stream we read the header from
+     * @throws BlenderFileException
+     *             this exception is thrown if the file header has some invalid data
+     */
+    private void readFileHeader() throws BlenderFileException {
+        byte[] identifier = new byte[7];
+        int bytesRead = this.readBytes(identifier);
+        if (bytesRead != 7) {
+            throw new BlenderFileException("Error reading header identifier. Only " + bytesRead + " bytes read and there should be 7!");
+        }
+        String strIdentifier = new String(identifier);
+        if (!"BLENDER".equals(strIdentifier)) {
+            throw new BlenderFileException("Wrong file identifier: " + strIdentifier + "! Should be 'BLENDER'!");
+        }
+        char pointerSizeSign = (char) this.readByte();
+        if (pointerSizeSign == '-') {
+            pointerSize = 8;
+        } else if (pointerSizeSign == '_') {
+            pointerSize = 4;
+        } else {
+            throw new BlenderFileException("Invalid pointer size character! Should be '_' or '-' and there is: " + pointerSizeSign);
+        }
+        endianess = (char) this.readByte();
+        if (endianess != 'v' && endianess != 'V') {
+            throw new BlenderFileException("Unknown endianess value! 'v' or 'V' expected and found: " + endianess);
+        }
+        byte[] versionNumber = new byte[3];
+        bytesRead = this.readBytes(versionNumber);
+        if (bytesRead != 3) {
+            throw new BlenderFileException("Error reading version numberr. Only " + bytesRead + " bytes read and there should be 3!");
+        }
+        this.versionNumber = new String(versionNumber);
+    }
+
+    @Override
+    public int read() throws IOException {
+        return this.readByte();
+    }
+
+    /**
+     * This method reads 1 byte from the stream.
+     * It works just in the way the read method does.
+     * It just not throw an exception because at this moment the whole file
+     * is loaded into buffer, so no need for IOException to be thrown.
+     * @return a byte from the stream (1 bytes read)
+     */
+    public int readByte() {
+        return cachedBuffer[position++] & 0xFF;
+    }
+
+    /**
+     * This method reads a bytes number big enough to fill the table.
+     * It does not throw exceptions so it is for internal use only.
+     * @param bytes
+     *            an array to be filled with data
+     * @return number of read bytes (a length of array actually)
+     */
+    private int readBytes(byte[] bytes) {
+        for (int i = 0; i < bytes.length; ++i) {
+            bytes[i] = (byte) this.readByte();
+        }
+        return bytes.length;
+    }
+
+    /**
+     * This method reads 2-byte number from the stream.
+     * @return a number from the stream (2 bytes read)
+     */
+    public int readShort() {
+        int part1 = this.readByte();
+        int part2 = this.readByte();
+        if (endianess == 'v') {
+            return (part2 << 8) + part1;
+        } else {
+            return (part1 << 8) + part2;
+        }
+    }
+
+    /**
+     * This method reads 4-byte number from the stream.
+     * @return a number from the stream (4 bytes read)
+     */
+    public int readInt() {
+        int part1 = this.readByte();
+        int part2 = this.readByte();
+        int part3 = this.readByte();
+        int part4 = this.readByte();
+        if (endianess == 'v') {
+            return (part4 << 24) + (part3 << 16) + (part2 << 8) + part1;
+        } else {
+            return (part1 << 24) + (part2 << 16) + (part3 << 8) + part4;
+        }
+    }
+
+    /**
+     * This method reads 4-byte floating point number (float) from the stream.
+     * @return a number from the stream (4 bytes read)
+     */
+    public float readFloat() {
+        int intValue = this.readInt();
+        return Float.intBitsToFloat(intValue);
+    }
+
+    /**
+     * This method reads 8-byte number from the stream.
+     * @return a number from the stream (8 bytes read)
+     */
+    public long readLong() {
+        long part1 = this.readInt();
+        long part2 = this.readInt();
+        long result = -1;
+        if (endianess == 'v') {
+            result = part2 << 32 | part1;
+        } else {
+            result = part1 << 32 | part2;
+        }
+        return result;
+    }
+
+    /**
+     * This method reads 8-byte floating point number (double) from the stream.
+     * @return a number from the stream (8 bytes read)
+     */
+    public double readDouble() {
+        long longValue = this.readLong();
+        return Double.longBitsToDouble(longValue);
+    }
+
+    /**
+     * This method reads the pointer value. Depending on the pointer size defined in the header, the stream reads either
+     * 4 or 8 bytes of data.
+     * @return the pointer value
+     */
+    public long readPointer() {
+        if (pointerSize == 4) {
+            return this.readInt();
+        }
+        return this.readLong();
+    }
+
+    /**
+     * This method reads the string. It assumes the string is terminated with zero in the stream.
+     * @return the string read from the stream
+     */
+    public String readString() {
+        StringBuilder stringBuilder = new StringBuilder();
+        int data = this.readByte();
+        while (data != 0) {
+            stringBuilder.append((char) data);
+            data = this.readByte();
+        }
+        return stringBuilder.toString();
+    }
+
+    /**
+     * This method sets the current position of the read cursor.
+     * @param position
+     *            the position of the read cursor
+     */
+    public void setPosition(int position) {
+        this.position = position;
+    }
+
+    /**
+     * This method returns the position of the read cursor.
+     * @return the position of the read cursor
+     */
+    public int getPosition() {
+        return position;
+    }
+
+    /**
+     * This method returns the blender version number where the file was created.
+     * @return blender version number
+     */
+    public String getVersionNumber() {
+        return versionNumber;
+    }
+
+    /**
+     * This method returns the size of the pointer.
+     * @return the size of the pointer
+     */
+    public int getPointerSize() {
+        return pointerSize;
+    }
+
+    /**
+     * This method aligns cursor position forward to a given amount of bytes.
+     * @param bytesAmount
+     *            the byte amount to which we aligh the cursor
+     */
+    public void alignPosition(int bytesAmount) {
+        if (bytesAmount <= 0) {
+            throw new IllegalArgumentException("Alignment byte number shoulf be positivbe!");
+        }
+        long move = position % bytesAmount;
+        if (move > 0) {
+            position += bytesAmount - move;
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        // this method is unimplemented because some loaders (ie. TGALoader) tend close the stream given from the outside
+        // because the images can be stored directly in the blender file then this stream is properly positioned and given to the loader
+        // to read the image file, that is why we do not want it to be closed before the reading is done
+        // and anyway this stream is only a cached buffer, so it does not hold any open connection to anything
+    }
+}

+ 203 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DnaBlockData.java

@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.file;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The data block containing the description of the file.
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class DnaBlockData {
+
+    private static final int             SDNA_ID = 'S' << 24 | 'D' << 16 | 'N' << 8 | 'A'; // SDNA
+    private static final int             NAME_ID = 'N' << 24 | 'A' << 16 | 'M' << 8 | 'E'; // NAME
+    private static final int             TYPE_ID = 'T' << 24 | 'Y' << 16 | 'P' << 8 | 'E'; // TYPE
+    private static final int             TLEN_ID = 'T' << 24 | 'L' << 16 | 'E' << 8 | 'N'; // TLEN
+    private static final int             STRC_ID = 'S' << 24 | 'T' << 16 | 'R' << 8 | 'C'; // STRC
+    /** Structures available inside the file. */
+    private final Structure[]            structures;
+    /** A map that helps finding a structure by type. */
+    private final Map<String, Structure> structuresMap;
+
+    /**
+     * Constructor. Loads the block from the given stream during instance creation.
+     * @param inputStream
+     *            the stream we read the block from
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             this exception is throw if the blend file is invalid or somehow corrupted
+     */
+    public DnaBlockData(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException {
+        int identifier;
+
+        // reading 'SDNA' identifier
+        identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
+
+        if (identifier != SDNA_ID) {
+            throw new BlenderFileException("Invalid identifier! '" + this.toString(SDNA_ID) + "' expected and found: " + this.toString(identifier));
+        }
+
+        // reading names
+        identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
+        if (identifier != NAME_ID) {
+            throw new BlenderFileException("Invalid identifier! '" + this.toString(NAME_ID) + "' expected and found: " + this.toString(identifier));
+        }
+        int amount = inputStream.readInt();
+        if (amount <= 0) {
+            throw new BlenderFileException("The names amount number should be positive!");
+        }
+        String[] names = new String[amount];
+        for (int i = 0; i < amount; ++i) {
+            names[i] = inputStream.readString();
+        }
+
+        // reding types
+        inputStream.alignPosition(4);
+        identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
+        if (identifier != TYPE_ID) {
+            throw new BlenderFileException("Invalid identifier! '" + this.toString(TYPE_ID) + "' expected and found: " + this.toString(identifier));
+        }
+        amount = inputStream.readInt();
+        if (amount <= 0) {
+            throw new BlenderFileException("The types amount number should be positive!");
+        }
+        String[] types = new String[amount];
+        for (int i = 0; i < amount; ++i) {
+            types[i] = inputStream.readString();
+        }
+
+        // reading lengths
+        inputStream.alignPosition(4);
+        identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
+        if (identifier != TLEN_ID) {
+            throw new BlenderFileException("Invalid identifier! '" + this.toString(TLEN_ID) + "' expected and found: " + this.toString(identifier));
+        }
+        int[] lengths = new int[amount];// theamount is the same as int types
+        for (int i = 0; i < amount; ++i) {
+            lengths[i] = inputStream.readShort();
+        }
+
+        // reading structures
+        inputStream.alignPosition(4);
+        identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
+        if (identifier != STRC_ID) {
+            throw new BlenderFileException("Invalid identifier! '" + this.toString(STRC_ID) + "' expected and found: " + this.toString(identifier));
+        }
+        amount = inputStream.readInt();
+        if (amount <= 0) {
+            throw new BlenderFileException("The structures amount number should be positive!");
+        }
+        structures = new Structure[amount];
+        structuresMap = new HashMap<String, Structure>(amount);
+        for (int i = 0; i < amount; ++i) {
+            structures[i] = new Structure(inputStream, names, types, blenderContext);
+            if (structuresMap.containsKey(structures[i].getType())) {
+                throw new BlenderFileException("Blend file seems to be corrupted! The type " + structures[i].getType() + " is defined twice!");
+            }
+            structuresMap.put(structures[i].getType(), structures[i]);
+        }
+    }
+
+    /**
+     * This method returns the amount of the structures.
+     * @return the amount of the structures
+     */
+    public int getStructuresCount() {
+        return structures.length;
+    }
+
+    /**
+     * This method returns the structure of the given index.
+     * @param index
+     *            the index of the structure
+     * @return the structure of the given index
+     */
+    public Structure getStructure(int index) {
+        try {
+            return (Structure) structures[index].clone();
+        } catch (CloneNotSupportedException e) {
+            throw new IllegalStateException("Structure should be clonable!!!", e);
+        }
+    }
+
+    /**
+     * This method returns a structure of the given name. If the name does not exists then null is returned.
+     * @param name
+     *            the name of the structure
+     * @return the required structure or null if the given name is inapropriate
+     */
+    public Structure getStructure(String name) {
+        try {
+            return (Structure) structuresMap.get(name).clone();
+        } catch (CloneNotSupportedException e) {
+            throw new IllegalStateException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * This method indicates if the structure of the given name exists.
+     * @param name
+     *            the name of the structure
+     * @return true if the structure exists and false otherwise
+     */
+    public boolean hasStructure(String name) {
+        return structuresMap.containsKey(name);
+    }
+
+    /**
+     * This method converts the given identifier code to string.
+     * @param code
+     *            the code taht is to be converted
+     * @return the string value of the identifier
+     */
+    private String toString(int code) {
+        char c1 = (char) ((code & 0xFF000000) >> 24);
+        char c2 = (char) ((code & 0xFF0000) >> 16);
+        char c3 = (char) ((code & 0xFF00) >> 8);
+        char c4 = (char) (code & 0xFF);
+        return String.valueOf(c1) + c2 + c3 + c4;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("=============== ").append(SDNA_ID).append('\n');
+        for (Structure structure : structures) {
+            stringBuilder.append(structure.toString()).append('\n');
+        }
+        return stringBuilder.append("===============").toString();
+    }
+}

+ 135 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DynamicArray.java

@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.file;
+
+/**
+ * An array that can be dynamically modified/
+ * @author Marcin Roguski
+ * @param <T>
+ *            the type of stored data in the array
+ */
+public class DynamicArray<T> implements Cloneable {
+
+    /** An array object that holds the required data. */
+    private T[]   array;
+    /**
+     * This table holds the sizes of dimetions of the dynamic table. It's length specifies the table dimension or a
+     * pointer level. For example: if tableSizes.length == 3 then it either specifies a dynamic table of fixed lengths:
+     * dynTable[a][b][c], where a,b,c are stored in the tableSizes table.
+     */
+    private int[] tableSizes;
+
+    /**
+     * Constructor. Builds an empty array of the specified sizes.
+     * @param tableSizes
+     *            the sizes of the table
+     * @throws IllegalArgumentException
+     *             an exception is thrown if one of the sizes is not a positive number
+     */
+    public DynamicArray(int[] tableSizes, T[] data) {
+        this.tableSizes = tableSizes;
+        int totalSize = 1;
+        for (int size : tableSizes) {
+            if (size <= 0) {
+                throw new IllegalArgumentException("The size of the table must be positive!");
+            }
+            totalSize *= size;
+        }
+        if (totalSize != data.length) {
+            throw new IllegalArgumentException("The size of the table does not match the size of the given data!");
+        }
+        this.array = data;
+    }
+
+    @Override
+    public Object clone() throws CloneNotSupportedException {
+        return super.clone();
+    }
+
+    /**
+     * This method returns a value on the specified position. The dimension of the table is not taken into
+     * consideration.
+     * @param position
+     *            the position of the data
+     * @return required data
+     */
+    public T get(int position) {
+        return array[position];
+    }
+
+    /**
+     * This method returns a value on the specified position in multidimensional array. Be careful not to exceed the
+     * table boundaries. Check the table's dimension first.
+     * @param position
+     *            the position of the data indices of data position
+     * @return required data required data
+     */
+    public T get(int... position) {
+        if (position.length != tableSizes.length) {
+            throw new ArrayIndexOutOfBoundsException("The table accepts " + tableSizes.length + " indexing number(s)!");
+        }
+        int index = 0;
+        for (int i = 0; i < position.length - 1; ++i) {
+            index += position[i] * tableSizes[i + 1];
+        }
+        index += position[position.length - 1];
+        return array[index];
+    }
+
+    /**
+     * This method returns the total amount of data stored in the array.
+     * @return the total amount of data stored in the array
+     */
+    public int getTotalSize() {
+        return array.length;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        if (array instanceof Character[]) {// in case of character array we convert it to String
+            for (int i = 0; i < array.length && (Character) array[i] != '\0'; ++i) {// strings are terminater with '0'
+                result.append(array[i]);
+            }
+        } else {
+            result.append('[');
+            for (int i = 0; i < array.length; ++i) {
+                result.append(array[i].toString());
+                if (i + 1 < array.length) {
+                    result.append(',');
+                }
+            }
+            result.append(']');
+        }
+        return result.toString();
+    }
+}

+ 327 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Field.java

@@ -0,0 +1,327 @@
+package com.jme3.scene.plugins.blender.file;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.file.Structure.DataType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a single field in the structure. It can be either a primitive type or a table or a reference to
+ * another structure.
+ * @author Marcin Roguski
+ */
+/* package */
+class Field implements Cloneable {
+
+    private static final int NAME_LENGTH = 24;
+    private static final int TYPE_LENGTH = 16;
+    /** The blender context. */
+    public BlenderContext    blenderContext;
+    /** The type of the field. */
+    public String            type;
+    /** The name of the field. */
+    public String            name;
+    /** The value of the field. Filled during data reading. */
+    public Object            value;
+    /** This variable indicates the level of the pointer. */
+    public int               pointerLevel;
+    /**
+     * This variable determines the sizes of the array. If the value is null the n the field is not an array.
+     */
+    public int[]             tableSizes;
+    /** This variable indicates if the field is a function pointer. */
+    public boolean           function;
+
+    /**
+     * Constructor. Saves the field data and parses its name.
+     * @param name
+     *            the name of the field
+     * @param type
+     *            the type of the field
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             this exception is thrown if the names contain errors
+     */
+    public Field(String name, String type, BlenderContext blenderContext) throws BlenderFileException {
+        this.type = type;
+        this.blenderContext = blenderContext;
+        this.parseField(new StringBuilder(name));
+    }
+
+    /**
+     * Copy constructor. Used in clone method. Copying is not full. The value in the new object is not set so that we
+     * have a clead empty copy of the filed to fill with data.
+     * @param field
+     *            the object that we copy
+     */
+    private Field(Field field) {
+        type = field.type;
+        name = field.name;
+        blenderContext = field.blenderContext;
+        pointerLevel = field.pointerLevel;
+        if (field.tableSizes != null) {
+            tableSizes = field.tableSizes.clone();
+        }
+        function = field.function;
+    }
+
+    @Override
+    public Object clone() throws CloneNotSupportedException {
+        return new Field(this);
+    }
+
+    /**
+     * This method fills the field wth data read from the input stream.
+     * @param blenderInputStream
+     *            the stream we read data from
+     * @throws BlenderFileException
+     *             an exception is thrown when the blend file is somehow invalid or corrupted
+     */
+    public void fill(BlenderInputStream blenderInputStream) throws BlenderFileException {
+        int dataToRead = 1;
+        if (tableSizes != null && tableSizes.length > 0) {
+            for (int size : tableSizes) {
+                if (size <= 0) {
+                    throw new BlenderFileException("The field " + name + " has invalid table size: " + size);
+                }
+                dataToRead *= size;
+            }
+        }
+        DataType dataType = pointerLevel == 0 ? DataType.getDataType(type, blenderContext) : DataType.POINTER;
+        switch (dataType) {
+            case POINTER:
+                if (dataToRead == 1) {
+                    Pointer pointer = new Pointer(pointerLevel, function, blenderContext);
+                    pointer.fill(blenderInputStream);
+                    value = pointer;
+                } else {
+                    Pointer[] data = new Pointer[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        Pointer pointer = new Pointer(pointerLevel, function, blenderContext);
+                        pointer.fill(blenderInputStream);
+                        data[i] = pointer;
+                    }
+                    value = new DynamicArray<Pointer>(tableSizes, data);
+                }
+                break;
+            case CHARACTER:
+                // character is also stored as a number, because sometimes the new blender version uses
+                // other number type instead of character as a field type
+                // and characters are very often used as byte number stores instead of real chars
+                if (dataToRead == 1) {
+                    value = Byte.valueOf((byte) blenderInputStream.readByte());
+                } else {
+                    Character[] data = new Character[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        data[i] = Character.valueOf((char) blenderInputStream.readByte());
+                    }
+                    value = new DynamicArray<Character>(tableSizes, data);
+                }
+                break;
+            case SHORT:
+                if (dataToRead == 1) {
+                    value = Integer.valueOf(blenderInputStream.readShort());
+                } else {
+                    Number[] data = new Number[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        data[i] = Integer.valueOf(blenderInputStream.readShort());
+                    }
+                    value = new DynamicArray<Number>(tableSizes, data);
+                }
+                break;
+            case INTEGER:
+                if (dataToRead == 1) {
+                    value = Integer.valueOf(blenderInputStream.readInt());
+                } else {
+                    Number[] data = new Number[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        data[i] = Integer.valueOf(blenderInputStream.readInt());
+                    }
+                    value = new DynamicArray<Number>(tableSizes, data);
+                }
+                break;
+            case LONG:
+                if (dataToRead == 1) {
+                    value = Long.valueOf(blenderInputStream.readLong());
+                } else {
+                    Number[] data = new Number[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        data[i] = Long.valueOf(blenderInputStream.readLong());
+                    }
+                    value = new DynamicArray<Number>(tableSizes, data);
+                }
+                break;
+            case FLOAT:
+                if (dataToRead == 1) {
+                    value = Float.valueOf(blenderInputStream.readFloat());
+                } else {
+                    Number[] data = new Number[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        data[i] = Float.valueOf(blenderInputStream.readFloat());
+                    }
+                    value = new DynamicArray<Number>(tableSizes, data);
+                }
+                break;
+            case DOUBLE:
+                if (dataToRead == 1) {
+                    value = Double.valueOf(blenderInputStream.readDouble());
+                } else {
+                    Number[] data = new Number[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        data[i] = Double.valueOf(blenderInputStream.readDouble());
+                    }
+                    value = new DynamicArray<Number>(tableSizes, data);
+                }
+                break;
+            case VOID:
+                break;
+            case STRUCTURE:
+                if (dataToRead == 1) {
+                    Structure structure = blenderContext.getDnaBlockData().getStructure(type);
+                    structure.fill(blenderContext.getInputStream());
+                    value = structure;
+                } else {
+                    Structure[] data = new Structure[dataToRead];
+                    for (int i = 0; i < dataToRead; ++i) {
+                        Structure structure = blenderContext.getDnaBlockData().getStructure(type);
+                        structure.fill(blenderContext.getInputStream());
+                        data[i] = structure;
+                    }
+                    value = new DynamicArray<Structure>(tableSizes, data);
+                }
+                break;
+            default:
+                throw new IllegalStateException("Unimplemented filling of type: " + type);
+        }
+    }
+
+    /**
+     * This method parses the field name to determine how the field should be used.
+     * @param nameBuilder
+     *            the name of the field (given as StringBuilder)
+     * @throws BlenderFileException
+     *             this exception is thrown if the names contain errors
+     */
+    private void parseField(StringBuilder nameBuilder) throws BlenderFileException {
+        this.removeWhitespaces(nameBuilder);
+        // veryfying if the name is a pointer
+        int pointerIndex = nameBuilder.indexOf("*");
+        while (pointerIndex >= 0) {
+            ++pointerLevel;
+            nameBuilder.deleteCharAt(pointerIndex);
+            pointerIndex = nameBuilder.indexOf("*");
+        }
+        // veryfying if the name is a function pointer
+        if (nameBuilder.indexOf("(") >= 0) {
+            function = true;
+            this.removeCharacter(nameBuilder, '(');
+            this.removeCharacter(nameBuilder, ')');
+        } else {
+            // veryfying if the name is a table
+            int tableStartIndex = 0;
+            List<Integer> lengths = new ArrayList<Integer>(3);// 3 dimensions will be enough in most cases
+            do {
+                tableStartIndex = nameBuilder.indexOf("[");
+                if (tableStartIndex > 0) {
+                    int tableStopIndex = nameBuilder.indexOf("]");
+                    if (tableStopIndex < 0) {
+                        throw new BlenderFileException("Invalid structure name: " + name);
+                    }
+                    try {
+                        lengths.add(Integer.valueOf(nameBuilder.substring(tableStartIndex + 1, tableStopIndex)));
+                    } catch (NumberFormatException e) {
+                        throw new BlenderFileException("Invalid structure name caused by invalid table length: " + name, e);
+                    }
+                    nameBuilder.delete(tableStartIndex, tableStopIndex + 1);
+                }
+            } while (tableStartIndex > 0);
+            if (!lengths.isEmpty()) {
+                tableSizes = new int[lengths.size()];
+                for (int i = 0; i < tableSizes.length; ++i) {
+                    tableSizes[i] = lengths.get(i).intValue();
+                }
+            }
+        }
+        name = nameBuilder.toString();
+    }
+
+    /**
+     * This method removes the required character from the text.
+     * @param text
+     *            the text we remove characters from
+     * @param toRemove
+     *            the character to be removed
+     */
+    private void removeCharacter(StringBuilder text, char toRemove) {
+        for (int i = 0; i < text.length(); ++i) {
+            if (text.charAt(i) == toRemove) {
+                text.deleteCharAt(i);
+                --i;
+            }
+        }
+    }
+
+    /**
+     * This method removes all whitespaces from the text.
+     * @param text
+     *            the text we remove whitespaces from
+     */
+    private void removeWhitespaces(StringBuilder text) {
+        for (int i = 0; i < text.length(); ++i) {
+            if (Character.isWhitespace(text.charAt(i))) {
+                text.deleteCharAt(i);
+                --i;
+            }
+        }
+    }
+
+    /**
+     * This method builds the full name of the field (with function, pointer and table indications).
+     * @return the full name of the field
+     */
+    /*package*/ String getFullName() {
+        StringBuilder result = new StringBuilder();
+        if (function) {
+            result.append('(');
+        }
+        for (int i = 0; i < pointerLevel; ++i) {
+            result.append('*');
+        }
+        result.append(name);
+        if (tableSizes != null) {
+            for (int i = 0; i < tableSizes.length; ++i) {
+                result.append('[').append(tableSizes[i]).append(']');
+            }
+        }
+        if (function) {
+            result.append(")()");
+        }
+        return result.toString();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append(this.getFullName());
+
+        // insert appropriate amount of spaces to format the output corrently
+        int nameLength = result.length();
+        result.append(' ');// at least one space is a must
+        for (int i = 1; i < NAME_LENGTH - nameLength; ++i) {// we start from i=1 because one space is already added
+            result.append(' ');
+        }
+        result.append(type);
+        nameLength = result.length();
+        for (int i = 0; i < NAME_LENGTH + TYPE_LENGTH - nameLength; ++i) {
+            result.append(' ');
+        }
+        if (value instanceof Character) {
+            result.append(" = ").append((int) ((Character) value).charValue());
+        } else {
+            result.append(" = ").append(value != null ? value.toString() : "null");
+        }
+        return result.toString();
+    }
+}

+ 189 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/FileBlockHeader.java

@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.file;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+
+/**
+ * A class that holds the header data of a file block. The file block itself is not implemented. This class holds its
+ * start position in the stream and using this the structure can fill itself with the proper data.
+ * @author Marcin Roguski
+ */
+public class FileBlockHeader {
+
+    public static final int BLOCK_TE00 = 'T' << 24 | 'E' << 16;                 // TE00
+    public static final int BLOCK_ME00 = 'M' << 24 | 'E' << 16;                 // ME00
+    public static final int BLOCK_SR00 = 'S' << 24 | 'R' << 16;                 // SR00
+    public static final int BLOCK_CA00 = 'C' << 24 | 'A' << 16;                 // CA00
+    public static final int BLOCK_LA00 = 'L' << 24 | 'A' << 16;                 // LA00
+    public static final int BLOCK_OB00 = 'O' << 24 | 'B' << 16;                 // OB00
+    public static final int BLOCK_MA00 = 'M' << 24 | 'A' << 16;                 // MA00
+    public static final int BLOCK_SC00 = 'S' << 24 | 'C' << 16;                 // SC00
+    public static final int BLOCK_WO00 = 'W' << 24 | 'O' << 16;                 // WO00
+    public static final int BLOCK_TX00 = 'T' << 24 | 'X' << 16;                 // TX00
+    public static final int BLOCK_IP00 = 'I' << 24 | 'P' << 16;                 // IP00
+    public static final int BLOCK_AC00 = 'A' << 24 | 'C' << 16;                 // AC00
+    public static final int BLOCK_GLOB = 'G' << 24 | 'L' << 16 | 'O' << 8 | 'B'; // GLOB
+    public static final int BLOCK_REND = 'R' << 24 | 'E' << 16 | 'N' << 8 | 'D'; // REND
+    public static final int BLOCK_DATA = 'D' << 24 | 'A' << 16 | 'T' << 8 | 'A'; // DATA
+    public static final int BLOCK_DNA1 = 'D' << 24 | 'N' << 16 | 'A' << 8 | '1'; // DNA1
+    public static final int BLOCK_ENDB = 'E' << 24 | 'N' << 16 | 'D' << 8 | 'B'; // ENDB
+    /** Identifier of the file-block [4 bytes]. */
+    private int             code;
+    /** Total length of the data after the file-block-header [4 bytes]. */
+    private int             size;
+    /**
+     * Memory address the structure was located when written to disk [4 or 8 bytes (defined in file header as a pointer
+     * size)].
+     */
+    private long            oldMemoryAddress;
+    /** Index of the SDNA structure [4 bytes]. */
+    private int             sdnaIndex;
+    /** Number of structure located in this file-block [4 bytes]. */
+    private int             count;
+    /** Start position of the block's data in the stream. */
+    private int             blockPosition;
+
+    /**
+     * Constructor. Loads the block header from the given stream during instance creation.
+     * @param inputStream
+     *            the stream we read the block header from
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             this exception is thrown when the pointer size is neither 4 nor 8
+     */
+    public FileBlockHeader(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException {
+        inputStream.alignPosition(4);
+        code = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
+        size = inputStream.readInt();
+        oldMemoryAddress = inputStream.readPointer();
+        sdnaIndex = inputStream.readInt();
+        count = inputStream.readInt();
+        blockPosition = inputStream.getPosition();
+        if (FileBlockHeader.BLOCK_DNA1 == code) {
+            blenderContext.setBlockData(new DnaBlockData(inputStream, blenderContext));
+        } else {
+            inputStream.setPosition(blockPosition + size);
+            blenderContext.addFileBlockHeader(Long.valueOf(oldMemoryAddress), this);
+        }
+    }
+
+    /**
+     * This method returns the structure described by the header filled with appropriate data.
+     * @param blenderContext
+     *            the blender context
+     * @return structure filled with data
+     * @throws BlenderFileException
+     */
+    public Structure getStructure(BlenderContext blenderContext) throws BlenderFileException {
+        blenderContext.getInputStream().setPosition(blockPosition);
+        Structure structure = blenderContext.getDnaBlockData().getStructure(sdnaIndex);
+        structure.fill(blenderContext.getInputStream());
+        return structure;
+    }
+
+    /**
+     * This method returns the code of this data block.
+     * @return the code of this data block
+     */
+    public int getCode() {
+        return code;
+    }
+
+    /**
+     * This method returns the size of the data stored in this block.
+     * @return the size of the data stored in this block
+     */
+    public int getSize() {
+        return size;
+    }
+
+    /**
+     * This method returns the sdna index.
+     * @return the sdna index
+     */
+    public int getSdnaIndex() {
+        return sdnaIndex;
+    }
+
+    /**
+     * This data returns the number of structure stored in the data block after this header.
+     * @return the number of structure stored in the data block after this header
+     */
+    public int getCount() {
+        return count;
+    }
+
+    /**
+     * This method returns the start position of the data block in the blend file stream.
+     * @return the start position of the data block
+     */
+    public int getBlockPosition() {
+        return blockPosition;
+    }
+
+    /**
+     * This method indicates if the block is the last block in the file.
+     * @return true if this block is the last one in the file nad false otherwise
+     */
+    public boolean isLastBlock() {
+        return FileBlockHeader.BLOCK_ENDB == code;
+    }
+
+    /**
+     * This method indicates if the block is the SDNA block.
+     * @return true if this block is the SDNA block and false otherwise
+     */
+    public boolean isDnaBlock() {
+        return FileBlockHeader.BLOCK_DNA1 == code;
+    }
+
+    @Override
+    public String toString() {
+        return "FILE BLOCK HEADER [" + this.codeToString(code) + " : " + size + " : " + oldMemoryAddress + " : " + sdnaIndex + " : " + count + "]";
+    }
+
+    /**
+     * This method transforms the coded bloch id into a string value.
+     * @param code
+     *            the id of the block
+     * @return the string value of the block id
+     */
+    protected String codeToString(int code) {
+        char c1 = (char) ((code & 0xFF000000) >> 24);
+        char c2 = (char) ((code & 0xFF0000) >> 16);
+        char c3 = (char) ((code & 0xFF00) >> 8);
+        char c4 = (char) (code & 0xFF);
+        return String.valueOf(c1) + c2 + c3 + c4;
+    }
+}

+ 189 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Pointer.java

@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.file;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+
+/**
+ * A class that represents a pointer of any level that can be stored in the file.
+ * @author Marcin Roguski
+ */
+public class Pointer {
+
+    /** The blender context. */
+    private BlenderContext blenderContext;
+    /** The level of the pointer. */
+    private int            pointerLevel;
+    /** The address in file it points to. */
+    private long           oldMemoryAddress;
+    /** This variable indicates if the field is a function pointer. */
+    public boolean         function;
+
+    /**
+     * Constructr. Stores the basic data about the pointer.
+     * @param pointerLevel
+     *            the level of the pointer
+     * @param function
+     *            this variable indicates if the field is a function pointer
+     * @param blenderContext
+     *            the repository f data; used in fetching the value that the pointer points
+     */
+    public Pointer(int pointerLevel, boolean function, BlenderContext blenderContext) {
+        this.pointerLevel = pointerLevel;
+        this.function = function;
+        this.blenderContext = blenderContext;
+    }
+
+    /**
+     * This method fills the pointer with its address value (it doesn't get the actual data yet. Use the 'fetch' method
+     * for this.
+     * @param inputStream
+     *            the stream we read the pointer value from
+     */
+    public void fill(BlenderInputStream inputStream) {
+        oldMemoryAddress = inputStream.readPointer();
+    }
+
+    /**
+     * This method fetches the data stored under the given address.
+     * @return the data read from the file
+     * @throws BlenderFileException
+     *             this exception is thrown when the blend file structure is somehow invalid or corrupted
+     */
+    public List<Structure> fetchData() throws BlenderFileException {
+        if (oldMemoryAddress == 0) {
+            throw new NullPointerException("The pointer points to nothing!");
+        }
+        List<Structure> structures = null;
+        FileBlockHeader dataFileBlock = blenderContext.getFileBlock(oldMemoryAddress);
+        if (dataFileBlock == null) {
+            throw new BlenderFileException("No data stored for address: " + oldMemoryAddress + ". Make sure you did not open the newer blender file with older blender version.");
+        }
+        BlenderInputStream inputStream = blenderContext.getInputStream();
+        if (pointerLevel > 1) {
+            int pointersAmount = dataFileBlock.getSize() / inputStream.getPointerSize() * dataFileBlock.getCount();
+            for (int i = 0; i < pointersAmount; ++i) {
+                inputStream.setPosition(dataFileBlock.getBlockPosition() + inputStream.getPointerSize() * i);
+                long oldMemoryAddress = inputStream.readPointer();
+                if (oldMemoryAddress != 0L) {
+                    Pointer p = new Pointer(pointerLevel - 1, function, blenderContext);
+                    p.oldMemoryAddress = oldMemoryAddress;
+                    if (structures == null) {
+                        structures = p.fetchData();
+                    } else {
+                        structures.addAll(p.fetchData());
+                    }
+                } else {
+                    // it is necessary to put null's if the pointer is null, ie. in materials array that is attached to the mesh, the index
+                    // of the material is important, that is why we need null's to indicate that some materials' slots are empty
+                    if (structures == null) {
+                        structures = new ArrayList<Structure>();
+                    }
+                    structures.add(null);
+                }
+            }
+        } else {
+            inputStream.setPosition(dataFileBlock.getBlockPosition());
+            structures = new ArrayList<Structure>(dataFileBlock.getCount());
+            for (int i = 0; i < dataFileBlock.getCount(); ++i) {
+                Structure structure = blenderContext.getDnaBlockData().getStructure(dataFileBlock.getSdnaIndex());
+                structure.fill(blenderContext.getInputStream());
+                structures.add(structure);
+            }
+            return structures;
+        }
+        return structures;
+    }
+
+    /**
+     * This method indicates if this pointer points to a function.
+     * @return <b>true</b> if this is a function pointer and <b>false</b> otherwise
+     */
+    public boolean isFunction() {
+        return function;
+    }
+
+    /**
+     * This method indicates if this is a null-pointer or not.
+     * @return <b>true</b> if the pointer is null and <b>false</b> otherwise
+     */
+    public boolean isNull() {
+        return oldMemoryAddress == 0;
+    }
+
+    /**
+     * This method indicates if this is a null-pointer or not.
+     * @return <b>true</b> if the pointer is not null and <b>false</b> otherwise
+     */
+    public boolean isNotNull() {
+        return oldMemoryAddress != 0;
+    }
+
+    /**
+     * This method returns the old memory address of the structure pointed by the pointer.
+     * @return the old memory address of the structure pointed by the pointer
+     */
+    public long getOldMemoryAddress() {
+        return oldMemoryAddress;
+    }
+
+    @Override
+    public String toString() {
+        return oldMemoryAddress == 0 ? "{$null$}" : "{$" + oldMemoryAddress + "$}";
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 + (int) (oldMemoryAddress ^ oldMemoryAddress >>> 32);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (this.getClass() != obj.getClass()) {
+            return false;
+        }
+        Pointer other = (Pointer) obj;
+        if (oldMemoryAddress != other.oldMemoryAddress) {
+            return false;
+        }
+        return true;
+    }
+}

+ 315 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Structure.java

@@ -0,0 +1,315 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.file;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.jme3.scene.plugins.blender.BlenderContext;
+
+/**
+ * A class representing a single structure in the file.
+ * @author Marcin Roguski
+ */
+public class Structure implements Cloneable {
+
+    /** The address of the block that fills the structure. */
+    private transient Long oldMemoryAddress;
+    /** The type of the structure. */
+    private String         type;
+    /**
+     * The fields of the structure. Each field consists of a pair: name-type.
+     */
+    private Field[]        fields;
+
+    /**
+     * Constructor that copies the data of the structure.
+     * @param structure
+     *            the structure to copy.
+     * @throws CloneNotSupportedException
+     *             this exception should never be thrown
+     */
+    private Structure(Structure structure) throws CloneNotSupportedException {
+        type = structure.type;
+        fields = new Field[structure.fields.length];
+        for (int i = 0; i < fields.length; ++i) {
+            fields[i] = (Field) structure.fields[i].clone();
+        }
+        oldMemoryAddress = structure.oldMemoryAddress;
+    }
+
+    /**
+     * Constructor. Loads the structure from the given stream during instance creation.
+     * @param inputStream
+     *            the stream we read the structure from
+     * @param names
+     *            the names from which the name of structure and its fields will be taken
+     * @param types
+     *            the names of types for the structure
+     * @param blenderContext
+     *            the blender context
+     * @throws BlenderFileException
+     *             this exception occurs if the amount of fields, defined in the file, is negative
+     */
+    public Structure(BlenderInputStream inputStream, String[] names, String[] types, BlenderContext blenderContext) throws BlenderFileException {
+        int nameIndex = inputStream.readShort();
+        type = types[nameIndex];
+        int fieldsAmount = inputStream.readShort();
+        if (fieldsAmount < 0) {
+            throw new BlenderFileException("The amount of fields of " + type + " structure cannot be negative!");
+        }
+        if (fieldsAmount > 0) {
+            fields = new Field[fieldsAmount];
+            for (int i = 0; i < fieldsAmount; ++i) {
+                int typeIndex = inputStream.readShort();
+                nameIndex = inputStream.readShort();
+                fields[i] = new Field(names[nameIndex], types[typeIndex], blenderContext);
+            }
+        }
+        oldMemoryAddress = Long.valueOf(-1L);
+    }
+
+    /**
+     * This method fills the structure with data.
+     * @param inputStream
+     *            the stream we read data from, its read cursor should be placed at the start position of the data for the
+     *            structure
+     * @throws BlenderFileException
+     *             an exception is thrown when the blend file is somehow invalid or corrupted
+     */
+    public void fill(BlenderInputStream inputStream) throws BlenderFileException {
+        int position = inputStream.getPosition();
+        inputStream.setPosition(position - 8 - inputStream.getPointerSize());
+        oldMemoryAddress = Long.valueOf(inputStream.readPointer());
+        inputStream.setPosition(position);
+        for (Field field : fields) {
+            field.fill(inputStream);
+        }
+    }
+
+    /**
+     * This method returns the value of the filed with a given name.
+     * @param fieldName
+     *            the name of the field
+     * @return the value of the field or null if no field with a given name is found
+     */
+    public Object getFieldValue(String fieldName) {
+        for (Field field : fields) {
+            if (field.name.equalsIgnoreCase(fieldName)) {
+                return field.value;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * This method returns the value of the filed with a given name. The structure is considered to have flat fields
+     * only (no substructures).
+     * @param fieldName
+     *            the name of the field
+     * @return the value of the field or null if no field with a given name is found
+     */
+    public Object getFlatFieldValue(String fieldName) {
+        for (Field field : fields) {
+            Object value = field.value;
+            if (field.name.equalsIgnoreCase(fieldName)) {
+                return value;
+            } else if (value instanceof Structure) {
+                value = ((Structure) value).getFlatFieldValue(fieldName);
+                if (value != null) {// we can compare references here, since we use one static object as a NULL field value
+                    return value;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * This methos should be used on structures that are of a 'ListBase' type. It creates a List of structures that are
+     * held by this structure within the blend file.
+     * @return a list of filled structures
+     * @throws BlenderFileException
+     *             this exception is thrown when the blend file structure is somehow invalid or corrupted
+     * @throws IllegalArgumentException
+     *             this exception is thrown if the type of the structure is not 'ListBase'
+     */
+    public List<Structure> evaluateListBase() throws BlenderFileException {
+        if (!"ListBase".equals(type)) {
+            throw new IllegalStateException("This structure is not of type: 'ListBase'");
+        }
+        Pointer first = (Pointer) this.getFieldValue("first");
+        Pointer last = (Pointer) this.getFieldValue("last");
+        long currentAddress = 0;
+        long lastAddress = last.getOldMemoryAddress();
+        List<Structure> result = new LinkedList<Structure>();
+        while (currentAddress != lastAddress) {
+            currentAddress = first.getOldMemoryAddress();
+            Structure structure = first.fetchData().get(0);
+            result.add(structure);
+            first = (Pointer) structure.getFlatFieldValue("next");
+        }
+        return result;
+    }
+
+    /**
+     * This method returns the type of the structure.
+     * @return the type of the structure
+     */
+    public String getType() {
+        return type;
+    }
+
+    /**
+     * This method returns the amount of fields for the current structure.
+     * @return the amount of fields for the current structure
+     */
+    public int getFieldsAmount() {
+        return fields.length;
+    }
+
+    /**
+     * This method returns the full field name of the given index.
+     * @param fieldIndex
+     *            the index of the field
+     * @return the full field name of the given index
+     */
+    public String getFieldFullName(int fieldIndex) {
+        return fields[fieldIndex].getFullName();
+    }
+
+    /**
+     * This method returns the field type of the given index.
+     * @param fieldIndex
+     *            the index of the field
+     * @return the field type of the given index
+     */
+    public String getFieldType(int fieldIndex) {
+        return fields[fieldIndex].type;
+    }
+
+    /**
+     * This method returns the address of the structure. The strucutre should be filled with data otherwise an exception
+     * is thrown.
+     * @return the address of the feature stored in this structure
+     */
+    public Long getOldMemoryAddress() {
+        if (oldMemoryAddress.longValue() == -1L) {
+            throw new IllegalStateException("Call the 'fill' method and fill the structure with data first!");
+        }
+        return oldMemoryAddress;
+    }
+
+    /**
+     * This method returns the name of the structure. If the structure has an ID field then the name is returned.
+     * Otherwise the name does not exists and the method returns null.
+     * @return the name of the structure read from the ID field or null
+     */
+    public String getName() {
+        Object fieldValue = this.getFieldValue("ID");
+        if (fieldValue instanceof Structure) {
+            Structure id = (Structure) fieldValue;
+            return id == null ? null : id.getFieldValue("name").toString().substring(2);// blender adds 2-charactes as a name prefix
+        }
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder("struct ").append(type).append(" {\n");
+        for (int i = 0; i < fields.length; ++i) {
+            result.append(fields[i].toString()).append('\n');
+        }
+        return result.append('}').toString();
+    }
+
+    @Override
+    public Object clone() throws CloneNotSupportedException {
+        return new Structure(this);
+    }
+
+    /**
+     * This enum enumerates all known data types that can be found in the blend file.
+     * @author Marcin Roguski (Kaelthas)
+     */
+    /* package */static enum DataType {
+
+        CHARACTER, SHORT, INTEGER, LONG, FLOAT, DOUBLE, VOID, STRUCTURE, POINTER;
+        /** The map containing the known primary types. */
+        private static final Map<String, DataType> PRIMARY_TYPES = new HashMap<String, DataType>(10);
+
+        static {
+            PRIMARY_TYPES.put("char", CHARACTER);
+            PRIMARY_TYPES.put("uchar", CHARACTER);
+            PRIMARY_TYPES.put("short", SHORT);
+            PRIMARY_TYPES.put("ushort", SHORT);
+            PRIMARY_TYPES.put("int", INTEGER);
+            PRIMARY_TYPES.put("long", LONG);
+            PRIMARY_TYPES.put("ulong", LONG);
+            PRIMARY_TYPES.put("uint64_t", LONG);
+            PRIMARY_TYPES.put("float", FLOAT);
+            PRIMARY_TYPES.put("double", DOUBLE);
+            PRIMARY_TYPES.put("void", VOID);
+        }
+
+        /**
+         * This method returns the data type that is appropriate to the given type name. WARNING! The type recognition
+         * is case sensitive!
+         * @param type
+         *            the type name of the data
+         * @param blenderContext
+         *            the blender context
+         * @return appropriate enum value to the given type name
+         * @throws BlenderFileException
+         *             this exception is thrown if the given type name does not exist in the blend file
+         */
+        public static DataType getDataType(String type, BlenderContext blenderContext) throws BlenderFileException {
+            DataType result = PRIMARY_TYPES.get(type);
+            if (result != null) {
+                return result;
+            }
+            if (blenderContext.getDnaBlockData().hasStructure(type)) {
+                return STRUCTURE;
+            }
+            throw new BlenderFileException("Unknown data type: " + type);
+        }
+
+        /**
+         * @return a collection of known primary types names
+         */
+        /* package */static Collection<String> getKnownPrimaryTypesNames() {
+            return PRIMARY_TYPES.keySet();
+        }
+    }
+}

+ 186 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java

@@ -0,0 +1,186 @@
+package com.jme3.scene.plugins.blender.landscape;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.light.AmbientLight;
+import com.jme3.light.Light;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.textures.ColorBand;
+import com.jme3.scene.plugins.blender.textures.CombinedTexture;
+import com.jme3.scene.plugins.blender.textures.ImageUtils;
+import com.jme3.scene.plugins.blender.textures.TextureHelper;
+import com.jme3.scene.plugins.blender.textures.TexturePixel;
+import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
+import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.TextureCubeMap;
+import com.jme3.util.SkyFactory;
+
+/**
+ * The class that allows to load the following: <li>the ambient light of the scene <li>the sky of the scene (with or without texture)
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class LandscapeHelper extends AbstractBlenderHelper {
+    private static final Logger LOGGER        = Logger.getLogger(LandscapeHelper.class.getName());
+
+    private static final int    SKYTYPE_BLEND = 1;
+    private static final int    SKYTYPE_REAL  = 2;
+    private static final int    SKYTYPE_PAPER = 4;
+
+    public LandscapeHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    /**
+     * Loads scene ambient light.
+     * @param worldStructure
+     *            the world's blender structure
+     * @return the scene's ambient light
+     */
+    public Light toAmbientLight(Structure worldStructure) {
+        LOGGER.fine("Loading ambient light.");
+        AmbientLight ambientLight = new AmbientLight();
+        float ambr = ((Number) worldStructure.getFieldValue("ambr")).floatValue();
+        float ambg = ((Number) worldStructure.getFieldValue("ambg")).floatValue();
+        float ambb = ((Number) worldStructure.getFieldValue("ambb")).floatValue();
+        ColorRGBA ambientLightColor = new ColorRGBA(ambr, ambg, ambb, 0.0f);
+        ambientLight.setColor(ambientLightColor);
+        LOGGER.log(Level.FINE, "Loaded ambient light: {0}.", ambientLightColor);
+        return ambientLight;
+    }
+
+    /**
+     * Loads the background color.
+     * @param worldStructure
+     *            the world's structure
+     * @return the horizon color of the world which is used as a background color.
+     */
+    public ColorRGBA toBackgroundColor(Structure worldStructure) {
+        float horr = ((Number) worldStructure.getFieldValue("horr")).floatValue();
+        float horg = ((Number) worldStructure.getFieldValue("horg")).floatValue();
+        float horb = ((Number) worldStructure.getFieldValue("horb")).floatValue();
+        return new ColorRGBA(horr, horg, horb, 1);
+    }
+
+    /**
+     * Loads scene's sky. Sky can be plain or textured.
+     * If no sky type is selected in blender then no sky is loaded.
+     * @param worldStructure
+     *            the world's structure
+     * @return the scene's sky
+     * @throws BlenderFileException
+     *             blender exception is thrown when problems with blender file occur
+     */
+    public Spatial toSky(Structure worldStructure) throws BlenderFileException {
+        int skytype = ((Number) worldStructure.getFieldValue("skytype")).intValue();
+        if (skytype == 0) {
+            return null;
+        }
+
+        LOGGER.fine("Loading sky.");
+        ColorRGBA horizontalColor = this.toBackgroundColor(worldStructure);
+
+        float zenr = ((Number) worldStructure.getFieldValue("zenr")).floatValue();
+        float zeng = ((Number) worldStructure.getFieldValue("zeng")).floatValue();
+        float zenb = ((Number) worldStructure.getFieldValue("zenb")).floatValue();
+        ColorRGBA zenithColor = new ColorRGBA(zenr, zeng, zenb, 1);
+
+        // jutr for this case load generated textures wheather user had set it or not because those might be needed to properly load the sky
+        boolean loadGeneratedTextures = blenderContext.getBlenderKey().isLoadGeneratedTextures();
+        blenderContext.getBlenderKey().setLoadGeneratedTextures(true);
+
+        TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
+        List<CombinedTexture> loadedTextures = null;
+        try {
+            loadedTextures = textureHelper.readTextureData(worldStructure, new float[] { horizontalColor.r, horizontalColor.g, horizontalColor.b, horizontalColor.a }, true);
+        } finally {
+            blenderContext.getBlenderKey().setLoadGeneratedTextures(loadGeneratedTextures);
+        }
+
+        TextureCubeMap texture = null;
+        if (loadedTextures != null && loadedTextures.size() > 0) {
+            if (loadedTextures.size() > 1) {
+                throw new IllegalStateException("There should be only one combined texture for sky!");
+            }
+            CombinedTexture combinedTexture = loadedTextures.get(0);
+            texture = combinedTexture.generateSkyTexture(horizontalColor, zenithColor, blenderContext);
+        } else {
+            LOGGER.fine("Preparing colors for colorband.");
+            int colorbandType = ColorBand.IPO_CARDINAL;
+            List<ColorRGBA> colorbandColors = new ArrayList<ColorRGBA>(3);
+            colorbandColors.add(horizontalColor);
+            if ((skytype & SKYTYPE_BLEND) != 0) {
+                if ((skytype & SKYTYPE_PAPER) != 0) {
+                    colorbandType = ColorBand.IPO_LINEAR;
+                }
+                if ((skytype & SKYTYPE_REAL) != 0) {
+                    colorbandColors.add(0, zenithColor);
+                }
+                colorbandColors.add(zenithColor);
+            }
+
+            int size = blenderContext.getBlenderKey().getSkyGeneratedTextureSize();
+
+            List<Integer> positions = new ArrayList<Integer>(colorbandColors.size());
+            positions.add(0);
+            if (colorbandColors.size() == 2) {
+                positions.add(size - 1);
+            } else if (colorbandColors.size() == 3) {
+                positions.add(size / 2);
+                positions.add(size - 1);
+            }
+
+            LOGGER.fine("Generating sky texture.");
+            float[][] values = new ColorBand(colorbandType, colorbandColors, positions, size).computeValues();
+
+            Image image = ImageUtils.createEmptyImage(Format.RGB8, size, size, 6);
+            PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat());
+            TexturePixel pixel = new TexturePixel();
+
+            LOGGER.fine("Creating side textures.");
+            int[] sideImagesIndexes = new int[] { 0, 1, 4, 5 };
+            for (int i : sideImagesIndexes) {
+                for (int y = 0; y < size; ++y) {
+                    pixel.red = values[y][0];
+                    pixel.green = values[y][1];
+                    pixel.blue = values[y][2];
+
+                    for (int x = 0; x < size; ++x) {
+                        pixelIO.write(image, i, pixel, x, y);
+                    }
+                }
+            }
+
+            LOGGER.fine("Creating top texture.");
+            pixelIO.read(image, 0, pixel, 0, image.getHeight() - 1);
+            for (int y = 0; y < size; ++y) {
+                for (int x = 0; x < size; ++x) {
+                    pixelIO.write(image, 3, pixel, x, y);
+                }
+            }
+
+            LOGGER.fine("Creating bottom texture.");
+            pixelIO.read(image, 0, pixel, 0, 0);
+            for (int y = 0; y < size; ++y) {
+                for (int x = 0; x < size; ++x) {
+                    pixelIO.write(image, 2, pixel, x, y);
+                }
+            }
+
+            texture = new TextureCubeMap(image);
+        }
+
+        LOGGER.fine("Sky texture created. Creating sky.");
+        return SkyFactory.createSky(blenderContext.getAssetManager(), texture, false);
+    }
+}

+ 118 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/lights/LightHelper.java

@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.lights;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.scene.LightNode;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+
+/**
+ * A class that is used in light calculations.
+ * @author Marcin Roguski
+ */
+public class LightHelper extends AbstractBlenderHelper {
+
+    private static final Logger LOGGER = Logger.getLogger(LightHelper.class.getName());
+
+    /**
+     * This constructor parses the given blender version and stores the result. Some functionalities may differ in
+     * different blender versions.
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public LightHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    public LightNode toLight(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
+        LightNode result = (LightNode) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
+        if (result != null) {
+            return result;
+        }
+        Light light = null;
+        int type = ((Number) structure.getFieldValue("type")).intValue();
+        switch (type) {
+            case 0:// Lamp
+                light = new PointLight();
+                float distance = ((Number) structure.getFieldValue("dist")).floatValue();
+                ((PointLight) light).setRadius(distance);
+                break;
+            case 1:// Sun
+                LOGGER.log(Level.WARNING, "'Sun' lamp is not supported in jMonkeyEngine.");
+                break;
+            case 2:// Spot
+                light = new SpotLight();
+                // range
+                ((SpotLight) light).setSpotRange(((Number) structure.getFieldValue("dist")).floatValue());
+                // outer angle
+                float outerAngle = ((Number) structure.getFieldValue("spotsize")).floatValue() * FastMath.DEG_TO_RAD * 0.5f;
+                ((SpotLight) light).setSpotOuterAngle(outerAngle);
+
+                // inner angle
+                float spotblend = ((Number) structure.getFieldValue("spotblend")).floatValue();
+                spotblend = FastMath.clamp(spotblend, 0, 1);
+                float innerAngle = outerAngle * (1 - spotblend);
+                ((SpotLight) light).setSpotInnerAngle(innerAngle);
+                break;
+            case 3:// Hemi
+                LOGGER.log(Level.WARNING, "'Hemi' lamp is not supported in jMonkeyEngine.");
+                break;
+            case 4:// Area
+                light = new DirectionalLight();
+                break;
+            default:
+                throw new BlenderFileException("Unknown light source type: " + type);
+        }
+        if (light != null) {
+            float r = ((Number) structure.getFieldValue("r")).floatValue();
+            float g = ((Number) structure.getFieldValue("g")).floatValue();
+            float b = ((Number) structure.getFieldValue("b")).floatValue();
+            light.setColor(new ColorRGBA(r, g, b, 1.0f));
+            result = new LightNode(null, light);
+        }
+        return result;
+    }
+}

+ 26 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/IAlphaMask.java

@@ -0,0 +1,26 @@
+package com.jme3.scene.plugins.blender.materials;
+
+/**
+ * An interface used in calculating alpha mask during particles' texture calculations.
+ * @author Marcin Roguski (Kaelthas)
+ */
+/* package */interface IAlphaMask {
+    /**
+     * This method sets the size of the texture's image.
+     * @param width
+     *            the width of the image
+     * @param height
+     *            the height of the image
+     */
+    void setImageSize(int width, int height);
+
+    /**
+     * This method returns the alpha value for the specified texture position.
+     * @param x
+     *            the X coordinate of the texture position
+     * @param y
+     *            the Y coordinate of the texture position
+     * @return the alpha value for the specified texture position
+     */
+    byte getAlpha(float x, float y);
+}

+ 327 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java

@@ -0,0 +1,327 @@
+package com.jme3.scene.plugins.blender.materials;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+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.math.Vector2f;
+import com.jme3.renderer.queue.RenderQueue.Bucket;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.VertexBuffer.Format;
+import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.materials.MaterialHelper.DiffuseShader;
+import com.jme3.scene.plugins.blender.materials.MaterialHelper.SpecularShader;
+import com.jme3.scene.plugins.blender.textures.CombinedTexture;
+import com.jme3.scene.plugins.blender.textures.TextureHelper;
+import com.jme3.texture.Texture;
+import com.jme3.util.BufferUtils;
+
+/**
+ * This class holds the data about the material.
+ * @author Marcin Roguski (Kaelthas)
+ */
+public final class MaterialContext {
+    private static final Logger              LOGGER     = Logger.getLogger(MaterialContext.class.getName());
+
+    // texture mapping types
+    public static final int                  MTEX_COL   = 0x01;
+    public static final int                  MTEX_NOR   = 0x02;
+    public static final int                  MTEX_SPEC  = 0x04;
+    public static final int                  MTEX_EMIT  = 0x40;
+    public static final int                  MTEX_ALPHA = 0x80;
+    public static final int                  MTEX_AMB   = 0x800;
+
+    /* package */final String                name;
+    /* package */final List<CombinedTexture> loadedTextures;
+
+    /* package */final ColorRGBA             diffuseColor;
+    /* package */final DiffuseShader         diffuseShader;
+    /* package */final SpecularShader        specularShader;
+    /* package */final ColorRGBA             specularColor;
+    /* package */final ColorRGBA             ambientColor;
+    /* package */final float                 shininess;
+    /* package */final boolean               shadeless;
+    /* package */final boolean               vertexColor;
+    /* package */final boolean               transparent;
+    /* package */final boolean               vTangent;
+    /* package */FaceCullMode                faceCullMode;
+
+    /* package */MaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
+        name = structure.getName();
+
+        int mode = ((Number) structure.getFieldValue("mode")).intValue();
+        shadeless = (mode & 0x4) != 0;
+        vertexColor = (mode & 0x80) != 0;
+        vTangent = (mode & 0x4000000) != 0; // NOTE: Requires tangents
+
+        int diff_shader = ((Number) structure.getFieldValue("diff_shader")).intValue();
+        diffuseShader = DiffuseShader.values()[diff_shader];
+
+        if (shadeless) {
+            float r = ((Number) structure.getFieldValue("r")).floatValue();
+            float g = ((Number) structure.getFieldValue("g")).floatValue();
+            float b = ((Number) structure.getFieldValue("b")).floatValue();
+            float alpha = ((Number) structure.getFieldValue("alpha")).floatValue();
+
+            diffuseColor = new ColorRGBA(r, g, b, alpha);
+            specularShader = null;
+            specularColor = ambientColor = null;
+            shininess = 0.0f;
+        } else {
+            diffuseColor = this.readDiffuseColor(structure, diffuseShader);
+
+            int spec_shader = ((Number) structure.getFieldValue("spec_shader")).intValue();
+            specularShader = SpecularShader.values()[spec_shader];
+            specularColor = this.readSpecularColor(structure);
+            float shininess = ((Number) structure.getFieldValue("har")).floatValue();// this is (probably) the specular hardness in blender
+            this.shininess = shininess > 0.0f ? shininess : MaterialHelper.DEFAULT_SHININESS;
+
+            float r = ((Number) structure.getFieldValue("ambr")).floatValue();
+            float g = ((Number) structure.getFieldValue("ambg")).floatValue();
+            float b = ((Number) structure.getFieldValue("ambb")).floatValue();
+            float alpha = ((Number) structure.getFieldValue("alpha")).floatValue();
+            ambientColor = new ColorRGBA(r, g, b, alpha);
+        }
+
+        TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
+        loadedTextures = textureHelper.readTextureData(structure, new float[] { diffuseColor.r, diffuseColor.g, diffuseColor.b, diffuseColor.a }, false);
+
+        // veryfying if the transparency is present
+        // (in blender transparent mask is 0x10000 but its better to verify it because blender can indicate transparency when
+        // it is not required
+        boolean transparent = false;
+        if (diffuseColor != null) {
+            transparent = diffuseColor.a < 1.0f;
+            if (loadedTextures.size() > 0) {// texutre covers the material color
+                diffuseColor.set(1, 1, 1, 1);
+            }
+        }
+        if (specularColor != null) {
+            transparent = transparent || specularColor.a < 1.0f;
+        }
+        if (ambientColor != null) {
+            transparent = transparent || ambientColor.a < 1.0f;
+        }
+        this.transparent = transparent;
+    }
+
+    /**
+     * Applies material to a given geometry.
+     * 
+     * @param geometry
+     *            the geometry
+     * @param geometriesOMA
+     *            the geometries OMA
+     * @param userDefinedUVCoordinates
+     *            UV coords defined by user
+     * @param blenderContext
+     *            the blender context
+     */
+    public void applyMaterial(Geometry geometry, Long geometriesOMA, LinkedHashMap<String, List<Vector2f>> userDefinedUVCoordinates, BlenderContext blenderContext) {
+        Material material = null;
+        if (shadeless) {
+            material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+
+            if (!transparent) {
+                diffuseColor.a = 1;
+            }
+
+            material.setColor("Color", diffuseColor);
+        } else {
+            material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Light/Lighting.j3md");
+            material.setBoolean("UseMaterialColors", Boolean.TRUE);
+
+            // setting the colors
+            material.setBoolean("Minnaert", diffuseShader == DiffuseShader.MINNAERT);
+            if (!transparent) {
+                diffuseColor.a = 1;
+            }
+            material.setColor("Diffuse", diffuseColor);
+
+            material.setBoolean("WardIso", specularShader == SpecularShader.WARDISO);
+            material.setColor("Specular", specularColor);
+            material.setFloat("Shininess", shininess);
+
+            material.setColor("Ambient", ambientColor);
+        }
+
+        // applying textures
+        if (loadedTextures != null && loadedTextures.size() > 0) {
+            int textureIndex = 0;
+            if (loadedTextures.size() > TextureHelper.TEXCOORD_TYPES.length) {
+                LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different textures. JME supports only {0} UV mappings.", TextureHelper.TEXCOORD_TYPES.length);
+            }
+            for (CombinedTexture combinedTexture : loadedTextures) {
+                if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) {
+                    combinedTexture.flatten(geometry, geometriesOMA, userDefinedUVCoordinates, blenderContext);
+
+                    this.setTexture(material, combinedTexture.getMappingType(), combinedTexture.getResultTexture());
+                    List<Vector2f> uvs = combinedTexture.getResultUVS();
+                    VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]);
+                    uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()])));
+                    geometry.getMesh().setBuffer(uvCoordsBuffer);
+                } else {
+                    LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length);
+                }
+            }
+        } else if (userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) {
+            LOGGER.fine("No textures found for the mesh, but UV coordinates are applied.");
+            int textureIndex = 0;
+            if (userDefinedUVCoordinates.size() > TextureHelper.TEXCOORD_TYPES.length) {
+                LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different UV coordinates for the mesh. JME supports only {0} UV coordinates buffers.", TextureHelper.TEXCOORD_TYPES.length);
+            }
+            for (Entry<String, List<Vector2f>> entry : userDefinedUVCoordinates.entrySet()) {
+                if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) {
+                    List<Vector2f> uvs = entry.getValue();
+                    VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]);
+                    uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()])));
+                    geometry.getMesh().setBuffer(uvCoordsBuffer);
+                } else {
+                    LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length);
+                }
+            }
+        }
+
+        // applying additional data
+        material.setName(name);
+        if (vertexColor) {
+            material.setBoolean(shadeless ? "VertexColor" : "UseVertexColor", true);
+        }
+        material.getAdditionalRenderState().setFaceCullMode(faceCullMode != null ? faceCullMode : blenderContext.getBlenderKey().getFaceCullMode());
+        if (transparent) {
+            material.setTransparent(true);
+            material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
+            geometry.setQueueBucket(Bucket.Transparent);
+        }
+
+        geometry.setMaterial(material);
+    }
+
+    /**
+     * Sets the texture to the given material.
+     * 
+     * @param material
+     *            the material that we add texture to
+     * @param mapTo
+     *            the texture mapping type
+     * @param texture
+     *            the added texture
+     */
+    private void setTexture(Material material, int mapTo, Texture texture) {
+        switch (mapTo) {
+            case MTEX_COL:
+                material.setTexture(shadeless ? MaterialHelper.TEXTURE_TYPE_COLOR : MaterialHelper.TEXTURE_TYPE_DIFFUSE, texture);
+                break;
+            case MTEX_NOR:
+                material.setTexture(MaterialHelper.TEXTURE_TYPE_NORMAL, texture);
+                break;
+            case MTEX_SPEC:
+                material.setTexture(MaterialHelper.TEXTURE_TYPE_SPECULAR, texture);
+                break;
+            case MTEX_EMIT:
+                material.setTexture(MaterialHelper.TEXTURE_TYPE_GLOW, texture);
+                break;
+            case MTEX_ALPHA:
+                if (!shadeless) {
+                    material.setTexture(MaterialHelper.TEXTURE_TYPE_ALPHA, texture);
+                } else {
+                    LOGGER.warning("JME does not support alpha map on unshaded material. Material name is " + name);
+                }
+                break;
+            case MTEX_AMB:
+                material.setTexture(MaterialHelper.TEXTURE_TYPE_LIGHTMAP, texture);
+                break;
+            default:
+                LOGGER.severe("Unknown mapping type: " + mapTo);
+        }
+    }
+
+    /**
+     * @return <b>true</b> if the material has at least one generated texture and <b>false</b> otherwise
+     */
+    public boolean hasGeneratedTextures() {
+        if (loadedTextures != null) {
+            for (CombinedTexture generatedTextures : loadedTextures) {
+                if (generatedTextures.hasGeneratedTextures()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * This method sets the face cull mode.
+     * @param faceCullMode
+     *            the face cull mode
+     */
+    public void setFaceCullMode(FaceCullMode faceCullMode) {
+        this.faceCullMode = faceCullMode;
+    }
+
+    /**
+     * This method returns the diffuse color.
+     * 
+     * @param materialStructure
+     *            the material structure
+     * @param diffuseShader
+     *            the diffuse shader
+     * @return the diffuse color
+     */
+    private ColorRGBA readDiffuseColor(Structure materialStructure, DiffuseShader diffuseShader) {
+        // bitwise 'or' of all textures mappings
+        int commonMapto = ((Number) materialStructure.getFieldValue("mapto")).intValue();
+
+        // diffuse color
+        float r = ((Number) materialStructure.getFieldValue("r")).floatValue();
+        float g = ((Number) materialStructure.getFieldValue("g")).floatValue();
+        float b = ((Number) materialStructure.getFieldValue("b")).floatValue();
+        float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue();
+        if ((commonMapto & 0x01) == 0x01) {// Col
+            return new ColorRGBA(r, g, b, alpha);
+        } else {
+            switch (diffuseShader) {
+                case FRESNEL:
+                case ORENNAYAR:
+                case TOON:
+                    break;// TODO: find what is the proper modification
+                case MINNAERT:
+                case LAMBERT:// TODO: check if that is correct
+                    float ref = ((Number) materialStructure.getFieldValue("ref")).floatValue();
+                    r *= ref;
+                    g *= ref;
+                    b *= ref;
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown diffuse shader type: " + diffuseShader.toString());
+            }
+            return new ColorRGBA(r, g, b, alpha);
+        }
+    }
+
+    /**
+     * This method returns a specular color used by the material.
+     * 
+     * @param materialStructure
+     *            the material structure filled with data
+     * @return a specular color used by the material
+     */
+    private ColorRGBA readSpecularColor(Structure materialStructure) {
+        float specularIntensity = ((Number) materialStructure.getFieldValue("spec")).floatValue();
+        float r = ((Number) materialStructure.getFieldValue("specr")).floatValue() * specularIntensity;
+        float g = ((Number) materialStructure.getFieldValue("specg")).floatValue() * specularIntensity;
+        float b = ((Number) materialStructure.getFieldValue("specb")).floatValue() * specularIntensity;
+        float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue();
+        return new ColorRGBA(r, g, b, alpha);
+    }
+}

+ 371 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialHelper.java

@@ -0,0 +1,371 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.materials;
+
+import java.nio.ByteBuffer;
+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.material.MatParam;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.shader.VarType;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.Texture;
+import com.jme3.util.BufferUtils;
+
+public class MaterialHelper extends AbstractBlenderHelper {
+    private static final Logger              LOGGER                = Logger.getLogger(MaterialHelper.class.getName());
+    protected static final float             DEFAULT_SHININESS     = 20.0f;
+
+    public static final String               TEXTURE_TYPE_COLOR    = "ColorMap";
+    public static final String               TEXTURE_TYPE_DIFFUSE  = "DiffuseMap";
+    public static final String               TEXTURE_TYPE_NORMAL   = "NormalMap";
+    public static final String               TEXTURE_TYPE_SPECULAR = "SpecularMap";
+    public static final String               TEXTURE_TYPE_GLOW     = "GlowMap";
+    public static final String               TEXTURE_TYPE_ALPHA    = "AlphaMap";
+    public static final String               TEXTURE_TYPE_LIGHTMAP = "LightMap";
+
+    public static final Integer              ALPHA_MASK_NONE       = Integer.valueOf(0);
+    public static final Integer              ALPHA_MASK_CIRCLE     = Integer.valueOf(1);
+    public static final Integer              ALPHA_MASK_CONE       = Integer.valueOf(2);
+    public static final Integer              ALPHA_MASK_HYPERBOLE  = Integer.valueOf(3);
+    protected final Map<Integer, IAlphaMask> alphaMasks            = new HashMap<Integer, IAlphaMask>();
+
+    /**
+     * The type of the material's diffuse shader.
+     */
+    public static enum DiffuseShader {
+        LAMBERT, ORENNAYAR, TOON, MINNAERT, FRESNEL
+    }
+
+    /**
+     * The type of the material's specular shader.
+     */
+    public static enum SpecularShader {
+        COOKTORRENCE, PHONG, BLINN, TOON, WARDISO
+    }
+
+    /**
+     * This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
+     * versions.
+     * 
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public MaterialHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+        // setting alpha masks
+        alphaMasks.put(ALPHA_MASK_NONE, new IAlphaMask() {
+            public void setImageSize(int width, int height) {
+            }
+
+            public byte getAlpha(float x, float y) {
+                return (byte) 255;
+            }
+        });
+        alphaMasks.put(ALPHA_MASK_CIRCLE, new IAlphaMask() {
+            private float   r;
+            private float[] center;
+
+            public void setImageSize(int width, int height) {
+                r = Math.min(width, height) * 0.5f;
+                center = new float[] { width * 0.5f, height * 0.5f };
+            }
+
+            public byte getAlpha(float x, float y) {
+                float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1])));
+                return (byte) (d >= r ? 0 : 255);
+            }
+        });
+        alphaMasks.put(ALPHA_MASK_CONE, new IAlphaMask() {
+            private float   r;
+            private float[] center;
+
+            public void setImageSize(int width, int height) {
+                r = Math.min(width, height) * 0.5f;
+                center = new float[] { width * 0.5f, height * 0.5f };
+            }
+
+            public byte getAlpha(float x, float y) {
+                float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1])));
+                return (byte) (d >= r ? 0 : -255.0f * d / r + 255.0f);
+            }
+        });
+        alphaMasks.put(ALPHA_MASK_HYPERBOLE, new IAlphaMask() {
+            private float   r;
+            private float[] center;
+
+            public void setImageSize(int width, int height) {
+                r = Math.min(width, height) * 0.5f;
+                center = new float[] { width * 0.5f, height * 0.5f };
+            }
+
+            public byte getAlpha(float x, float y) {
+                float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1]))) / r;
+                return d >= 1.0f ? 0 : (byte) ((-FastMath.sqrt((2.0f - d) * d) + 1.0f) * 255.0f);
+            }
+        });
+    }
+
+    /**
+     * This method converts the material structure to jme Material.
+     * @param structure
+     *            structure with material data
+     * @param blenderContext
+     *            the blender context
+     * @return jme material
+     * @throws BlenderFileException
+     *             an exception is throw when problems with blend file occur
+     */
+    public MaterialContext toMaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
+        LOGGER.log(Level.FINE, "Loading material.");
+        MaterialContext result = (MaterialContext) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
+        if (result != null) {
+            return result;
+        }
+
+        result = new MaterialContext(structure, blenderContext);
+        LOGGER.log(Level.FINE, "Material''s name: {0}", result.name);
+        blenderContext.addLoadedFeatures(structure.getOldMemoryAddress(), structure.getName(), structure, result);
+        return result;
+    }
+
+    /**
+     * This method converts the given material into particles-usable material.
+     * The texture and glow color are being copied.
+     * The method assumes it receives the Lighting type of material.
+     * @param material
+     *            the source material
+     * @param blenderContext
+     *            the blender context
+     * @return material converted into particles-usable material
+     */
+    public Material getParticlesMaterial(Material material, Integer alphaMaskIndex, BlenderContext blenderContext) {
+        Material result = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
+
+        // copying texture
+        MatParam diffuseMap = material.getParam("DiffuseMap");
+        if (diffuseMap != null) {
+            Texture texture = ((Texture) diffuseMap.getValue()).clone();
+
+            // applying alpha mask to the texture
+            Image image = texture.getImage();
+            ByteBuffer sourceBB = image.getData(0);
+            sourceBB.rewind();
+            int w = image.getWidth();
+            int h = image.getHeight();
+            ByteBuffer bb = BufferUtils.createByteBuffer(w * h * 4);
+            IAlphaMask iAlphaMask = alphaMasks.get(alphaMaskIndex);
+            iAlphaMask.setImageSize(w, h);
+
+            for (int x = 0; x < w; ++x) {
+                for (int y = 0; y < h; ++y) {
+                    bb.put(sourceBB.get());
+                    bb.put(sourceBB.get());
+                    bb.put(sourceBB.get());
+                    bb.put(iAlphaMask.getAlpha(x, y));
+                }
+            }
+
+            image = new Image(Format.RGBA8, w, h, bb);
+            texture.setImage(image);
+
+            result.setTextureParam("Texture", VarType.Texture2D, texture);
+        }
+
+        // copying glow color
+        MatParam glowColor = material.getParam("GlowColor");
+        if (glowColor != null) {
+            ColorRGBA color = (ColorRGBA) glowColor.getValue();
+            result.setParam("GlowColor", VarType.Vector3, color);
+        }
+        return result;
+    }
+
+    /**
+     * This method returns the table of materials connected to the specified structure. The given structure can be of any type (ie. mesh or
+     * curve) but needs to have 'mat' field/
+     * 
+     * @param structureWithMaterials
+     *            the structure containing the mesh data
+     * @param blenderContext
+     *            the blender context
+     * @return a list of vertices colors, each color belongs to a single vertex
+     * @throws BlenderFileException
+     *             this exception is thrown when the blend file structure is somehow invalid or corrupted
+     */
+    public MaterialContext[] getMaterials(Structure structureWithMaterials, BlenderContext blenderContext) throws BlenderFileException {
+        Pointer ppMaterials = (Pointer) structureWithMaterials.getFieldValue("mat");
+        MaterialContext[] materials = null;
+        if (ppMaterials.isNotNull()) {
+            List<Structure> materialStructures = ppMaterials.fetchData();
+            if (materialStructures != null && materialStructures.size() > 0) {
+                MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
+                materials = new MaterialContext[materialStructures.size()];
+                int i = 0;
+                for (Structure s : materialStructures) {
+                    materials[i++] = s == null ? null : materialHelper.toMaterialContext(s, blenderContext);
+                }
+            }
+        }
+        return materials;
+    }
+
+    /**
+     * This method converts rgb values to hsv values.
+     * 
+     * @param r
+     *            red value of the color
+     * @param g
+     *            green value of the color
+     * @param b
+     *            blue value of the color
+     * @param hsv
+     *            hsv values of a color (this table contains the result of the transformation)
+     */
+    public void rgbToHsv(float r, float g, float b, float[] hsv) {
+        float cmax = r;
+        float cmin = r;
+        cmax = g > cmax ? g : cmax;
+        cmin = g < cmin ? g : cmin;
+        cmax = b > cmax ? b : cmax;
+        cmin = b < cmin ? b : cmin;
+
+        hsv[2] = cmax; /* value */
+        if (cmax != 0.0) {
+            hsv[1] = (cmax - cmin) / cmax;
+        } else {
+            hsv[1] = 0.0f;
+            hsv[0] = 0.0f;
+        }
+        if (hsv[1] == 0.0) {
+            hsv[0] = -1.0f;
+        } else {
+            float cdelta = cmax - cmin;
+            float rc = (cmax - r) / cdelta;
+            float gc = (cmax - g) / cdelta;
+            float bc = (cmax - b) / cdelta;
+            if (r == cmax) {
+                hsv[0] = bc - gc;
+            } else if (g == cmax) {
+                hsv[0] = 2.0f + rc - bc;
+            } else {
+                hsv[0] = 4.0f + gc - rc;
+            }
+            hsv[0] *= 60.0f;
+            if (hsv[0] < 0.0f) {
+                hsv[0] += 360.0f;
+            }
+        }
+
+        hsv[0] /= 360.0f;
+        if (hsv[0] < 0.0f) {
+            hsv[0] = 0.0f;
+        }
+    }
+
+    /**
+     * This method converts rgb values to hsv values.
+     * 
+     * @param h
+     *            hue
+     * @param s
+     *            saturation
+     * @param v
+     *            value
+     * @param rgb
+     *            rgb result vector (should have 3 elements)
+     */
+    public void hsvToRgb(float h, float s, float v, float[] rgb) {
+        h *= 360.0f;
+        if (s == 0.0) {
+            rgb[0] = rgb[1] = rgb[2] = v;
+        } else {
+            if (h == 360) {
+                h = 0;
+            } else {
+                h /= 60;
+            }
+            int i = (int) Math.floor(h);
+            float f = h - i;
+            float p = v * (1.0f - s);
+            float q = v * (1.0f - s * f);
+            float t = v * (1.0f - s * (1.0f - f));
+            switch (i) {
+                case 0:
+                    rgb[0] = v;
+                    rgb[1] = t;
+                    rgb[2] = p;
+                    break;
+                case 1:
+                    rgb[0] = q;
+                    rgb[1] = v;
+                    rgb[2] = p;
+                    break;
+                case 2:
+                    rgb[0] = p;
+                    rgb[1] = v;
+                    rgb[2] = t;
+                    break;
+                case 3:
+                    rgb[0] = p;
+                    rgb[1] = q;
+                    rgb[2] = v;
+                    break;
+                case 4:
+                    rgb[0] = t;
+                    rgb[1] = p;
+                    rgb[2] = v;
+                    break;
+                case 5:
+                    rgb[0] = v;
+                    rgb[1] = p;
+                    rgb[2] = q;
+                    break;
+            }
+        }
+    }
+}

+ 88 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshContext.java

@@ -0,0 +1,88 @@
+package com.jme3.scene.plugins.blender.meshes;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.jme3.scene.Geometry;
+
+/**
+ * Class that holds information about the mesh.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class MeshContext {
+    /** A map between material index and the geometry. */
+    private Map<Integer, List<Geometry>>              geometries = new HashMap<Integer, List<Geometry>>();
+    /** The vertex reference map. */
+    private Map<Integer, Map<Integer, List<Integer>>> vertexReferenceMap;
+
+    /**
+     * Adds a geometry for the specified material index.
+     * @param materialIndex
+     *            the material index
+     * @param geometry
+     *            the geometry
+     */
+    public void putGeometry(Integer materialIndex, Geometry geometry) {
+        List<Geometry> geomList = geometries.get(materialIndex);
+        if (geomList == null) {
+            geomList = new ArrayList<Geometry>();
+            geometries.put(materialIndex, geomList);
+        }
+        geomList.add(geometry);
+    }
+
+    /**
+     * @param materialIndex
+     *            the material index
+     * @return vertices amount that is used by mesh with the specified material
+     */
+    public int getVertexCount(int materialIndex) {
+        int result = 0;
+        for (Geometry geometry : geometries.get(materialIndex)) {
+            result += geometry.getVertexCount();
+        }
+        return result;
+    }
+
+    /**
+     * Returns material index for the geometry.
+     * @param geometry
+     *            the geometry
+     * @return material index
+     * @throws IllegalStateException
+     *             this exception is thrown when no material is found for the specified geometry
+     */
+    public int getMaterialIndex(Geometry geometry) {
+        for (Entry<Integer, List<Geometry>> entry : geometries.entrySet()) {
+            for (Geometry g : entry.getValue()) {
+                if (g.equals(geometry)) {
+                    return entry.getKey();
+                }
+            }
+        }
+        throw new IllegalStateException("Cannot find material index for the given geometry: " + geometry);
+    }
+
+    /**
+     * This method returns the vertex reference map.
+     * 
+     * @return the vertex reference map
+     */
+    public Map<Integer, List<Integer>> getVertexReferenceMap(int materialIndex) {
+        return vertexReferenceMap.get(materialIndex);
+    }
+
+    /**
+     * This method sets the vertex reference map.
+     * 
+     * @param vertexReferenceMap
+     *            the vertex reference map
+     */
+    public void setVertexReferenceMap(Map<Integer, Map<Integer, List<Integer>>> vertexReferenceMap) {
+        this.vertexReferenceMap = vertexReferenceMap;
+    }
+}

+ 243 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java

@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2009-2012 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.blender.meshes;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.asset.BlenderKey.FeaturesToLoad;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Mesh.Mode;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.VertexBuffer.Format;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
+import com.jme3.scene.plugins.blender.BlenderContext;
+import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
+import com.jme3.scene.plugins.blender.file.BlenderFileException;
+import com.jme3.scene.plugins.blender.file.Pointer;
+import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.materials.MaterialContext;
+import com.jme3.scene.plugins.blender.materials.MaterialHelper;
+import com.jme3.scene.plugins.blender.meshes.builders.MeshBuilder;
+import com.jme3.scene.plugins.blender.objects.Properties;
+import com.jme3.scene.plugins.blender.textures.TextureHelper;
+import com.jme3.util.BufferUtils;
+
+/**
+ * A class that is used in mesh calculations.
+ * 
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class MeshHelper extends AbstractBlenderHelper {
+    private static final Logger LOGGER                   = Logger.getLogger(MeshHelper.class.getName());
+
+    /** A type of UV data layer in traditional faced mesh (triangles or quads). */
+    public static final int     UV_DATA_LAYER_TYPE_FMESH = 5;
+    /** A type of UV data layer in bmesh type. */
+    public static final int     UV_DATA_LAYER_TYPE_BMESH = 16;
+    /** A material used for single lines and points. */
+    private Material            blackUnshadedMaterial;
+
+    /**
+     * This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
+     * versions.
+     * 
+     * @param blenderVersion
+     *            the version read from the blend file
+     * @param blenderContext
+     *            the blender context
+     */
+    public MeshHelper(String blenderVersion, BlenderContext blenderContext) {
+        super(blenderVersion, blenderContext);
+    }
+
+    /**
+     * This method reads converts the given structure into mesh. The given structure needs to be filled with the appropriate data.
+     * 
+     * @param structure
+     *            the structure we read the mesh from
+     * @return the mesh feature
+     * @throws BlenderFileException
+     */
+    @SuppressWarnings("unchecked")
+    public List<Geometry> toMesh(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
+        List<Geometry> geometries = (List<Geometry>) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
+        if (geometries != null) {
+            List<Geometry> copiedGeometries = new ArrayList<Geometry>(geometries.size());
+            for (Geometry geometry : geometries) {
+                copiedGeometries.add(geometry.clone());
+            }
+            return copiedGeometries;
+        }
+
+        String name = structure.getName();
+        MeshContext meshContext = new MeshContext();
+        LOGGER.log(Level.FINE, "Reading mesh: {0}.", name);
+
+        LOGGER.fine("Loading materials.");
+        MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
+        MaterialContext[] materials = null;
+        if ((blenderContext.getBlenderKey().getFeaturesToLoad() & FeaturesToLoad.MATERIALS) != 0) {
+            materials = materialHelper.getMaterials(structure, blenderContext);
+        }
+
+        LOGGER.fine("Reading vertices.");
+        MeshBuilder meshBuilder = new MeshBuilder(structure, materials, blenderContext);
+        if (meshBuilder.isEmpty()) {
+            LOGGER.fine("The geometry is empty.");
+            geometries = new ArrayList<Geometry>(0);
+            blenderContext.addLoadedFeatures(structure.getOldMemoryAddress(), structure.getName(), structure, geometries);
+            blenderContext.setMeshContext(structure.getOldMemoryAddress(), meshContext);
+            return geometries;
+        }
+
+        meshContext.setVertexReferenceMap(meshBuilder.getVertexReferenceMap());
+
+        LOGGER.fine("Reading vertices groups (from the Object structure).");
+        Structure parent = blenderContext.peekParent();
+        Structure defbase = (Structure) parent.getFieldValue("defbase");
+        List<Structure> defs = defbase.evaluateListBase();
+        String[] verticesGroups = new String[defs.size()];
+        int defIndex = 0;
+        for (Structure def : defs) {
+            verticesGroups[defIndex++] = def.getFieldValue("name").toString();
+        }
+
+        LOGGER.fine("Reading custom properties.");
+        Properties properties = this.loadProperties(structure, blenderContext);
+
+        LOGGER.fine("Generating meshes.");
+        Map<Integer, List<Mesh>> meshes = meshBuilder.buildMeshes();
+        geometries = new ArrayList<Geometry>(meshes.size());
+        for (Entry<Integer, List<Mesh>> meshEntry : meshes.entrySet()) {
+            int materialIndex = meshEntry.getKey();
+            for (Mesh mesh : meshEntry.getValue()) {
+                LOGGER.fine("Preparing the result part.");
+                Geometry geometry = new Geometry(name + (geometries.size() + 1), mesh);
+                if (properties != null && properties.getValue() != null) {
+                    this.applyProperties(geometry, properties);
+                }
+                geometries.add(geometry);
+                meshContext.putGeometry(materialIndex, geometry);
+            }
+        }
+
+        // store the data in blender context before applying the material
+        blenderContext.addLoadedFeatures(structure.getOldMemoryAddress(), structure.getName(), structure, geometries);
+        blenderContext.setMeshContext(structure.getOldMemoryAddress(), meshContext);
+
+        // apply materials only when all geometries are in place
+        if (materials != null) {
+            for (Geometry geometry : geometries) {
+                int materialNumber = meshContext.getMaterialIndex(geometry);
+                if (materialNumber < 0) {
+                    geometry.setMaterial(this.getBlackUnshadedMaterial(blenderContext));
+                } else if (materials[materialNumber] != null) {
+                    LinkedHashMap<String, List<Vector2f>> uvCoordinates = meshBuilder.getUVCoordinates(materialNumber);
+                    MaterialContext materialContext = materials[materialNumber];
+                    materialContext.applyMaterial(geometry, structure.getOldMemoryAddress(), uvCoordinates, blenderContext);
+                } else {
+                    geometry.setMaterial(blenderContext.getDefaultMaterial());
+                    LOGGER.warning("The importer came accross mesh that points to a null material. Default material is used to prevent loader from crashing, " + "but the model might look not the way it should. Sometimes blender does not assign materials properly. " + "Enter the edit mode and assign materials once more to your faces.");
+                }
+            }
+        } else {
+            // add UV coordinates if they are defined even if the material is not applied to the model
+            List<VertexBuffer> uvCoordsBuffer = null;
+            if (meshBuilder.hasUVCoordinates()) {
+                Map<String, List<Vector2f>> uvs = meshBuilder.getUVCoordinates(0);
+                if (uvs != null && uvs.size() > 0) {
+                    uvCoordsBuffer = new ArrayList<VertexBuffer>(uvs.size());
+                    int uvIndex = 0;
+                    for (Entry<String, List<Vector2f>> entry : uvs.entrySet()) {
+                        VertexBuffer buffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[uvIndex++]);
+                        buffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(entry.getValue().toArray(new Vector2f[uvs.size()])));
+                        uvCoordsBuffer.add(buffer);
+                    }
+                }
+            }
+
+            for (Geometry geometry : geometries) {
+                Mode mode = geometry.getMesh().getMode();
+                if (mode != Mode.Triangles && mode != Mode.TriangleFan && mode != Mode.TriangleStrip) {
+                    geometry.setMaterial(this.getBlackUnshadedMaterial(blenderContext));
+                } else {
+                    Material defaultMaterial = blenderContext.getDefaultMaterial();
+                    if(geometry.getMesh().getBuffer(Type.Color) != null) {
+                        defaultMaterial = defaultMaterial.clone();
+                        defaultMaterial.setBoolean("VertexColor", true);
+                    }
+                    geometry.setMaterial(defaultMaterial);
+                }
+                if (uvCoordsBuffer != null) {
+                    for (VertexBuffer buffer : uvCoordsBuffer) {
+                        geometry.getMesh().setBuffer(buffer);
+                    }
+                }
+            }
+        }
+
+        return geometries;
+    }
+
+    /**
+     * Tells if the given mesh structure supports BMesh.
+     * 
+     * @param meshStructure
+     *            the mesh structure
+     * @return <b>true</b> if BMesh is supported and <b>false</b> otherwise
+     */
+    public boolean isBMeshCompatible(Structure meshStructure) {
+        Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop");
+        Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly");
+        return pMLoop != null && pMPoly != null && pMLoop.isNotNull() && pMPoly.isNotNull();
+    }
+
+    private synchronized Material getBlackUnshadedMaterial(BlenderContext blenderContext) {
+        if (blackUnshadedMaterial == null) {
+            blackUnshadedMaterial = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+            blackUnshadedMaterial.setColor("Color", ColorRGBA.Black);
+        }
+        return blackUnshadedMaterial;
+    }
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно