Browse Source

Merge pull request #2 from jMonkeyEngine/master

update fork from jMonkeyEngine to jmecn
Yan 7 năm trước cách đây
mục cha
commit
189c8a5a6f
100 tập tin đã thay đổi với 5652 bổ sung3469 xóa
  1. 1 0
      .gitignore
  2. 4 0
      .travis.yml
  3. 8 1
      appveyor.yml
  4. 7 7
      build.gradle
  5. 3 0
      common.gradle
  6. 2 2
      gradle.properties
  7. BIN
      gradle/wrapper/gradle-wrapper.jar
  8. 2 2
      gradle/wrapper/gradle-wrapper.properties
  9. 15 7
      gradlew
  10. 0 6
      gradlew.bat
  11. 1 1
      jme3-android/build.gradle
  12. 3 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java
  13. 5 0
      jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
  14. 8 2
      jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
  15. 22 12
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java
  16. 20 17
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java
  17. 67 66
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ImageLoader.java
  18. 22 2
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TextureHelper.java
  19. 11 1
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderAWT.java
  20. BIN
      jme3-bullet-native/libs/native/linux/x86/libbulletjme.so
  21. BIN
      jme3-bullet-native/libs/native/linux/x86_64/libbulletjme.so
  22. BIN
      jme3-bullet-native/libs/native/osx/x86/libbulletjme.dylib
  23. BIN
      jme3-bullet-native/libs/native/osx/x86_64/libbulletjme.dylib
  24. BIN
      jme3-bullet-native/libs/native/windows/x86/bulletjme.dll
  25. BIN
      jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll
  26. 1 1
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_PhysicsCollisionEvent.cpp
  27. 252 13
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsCharacter.cpp
  28. 127 11
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsCharacter.h
  29. 37 1
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsRigidBody.cpp
  30. 16 0
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsRigidBody.h
  31. 152 13
      jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsCharacter.java
  32. 15 1
      jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsRigidBody.java
  33. 10 2
      jme3-bullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java
  34. 369 365
      jme3-core/src/main/java/com/jme3/animation/AnimChannel.java
  35. 19 1
      jme3-core/src/main/java/com/jme3/animation/Animation.java
  36. 4 6
      jme3-core/src/main/java/com/jme3/animation/AnimationUtils.java
  37. 7 5
      jme3-core/src/main/java/com/jme3/animation/Bone.java
  38. 3 1
      jme3-core/src/main/java/com/jme3/animation/Skeleton.java
  39. 58 86
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  40. 43 13
      jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java
  41. 3 2
      jme3-core/src/main/java/com/jme3/asset/ImplHandler.java
  42. 715 720
      jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java
  43. 93 89
      jme3-core/src/main/java/com/jme3/cinematic/KeyFrame.java
  44. 32 3
      jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java
  45. 146 0
      jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java
  46. 491 491
      jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
  47. 230 229
      jme3-core/src/main/java/com/jme3/cinematic/events/SoundEvent.java
  48. 1 1
      jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
  49. 39 4
      jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java
  50. 44 40
      jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java
  51. 0 176
      jme3-core/src/main/java/com/jme3/environment/generation/IrradianceMapGenerator.java
  52. 117 0
      jme3-core/src/main/java/com/jme3/environment/generation/IrradianceSphericalHarmonicsGenerator.java
  53. 191 101
      jme3-core/src/main/java/com/jme3/environment/generation/PrefilteredEnvMapFaceGenerator.java
  54. 25 9
      jme3-core/src/main/java/com/jme3/environment/util/CubeMapWrapper.java
  55. 57 257
      jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java
  56. 1 39
      jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java
  57. 11 5
      jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java
  58. 167 89
      jme3-core/src/main/java/com/jme3/input/FlyByCamera.java
  59. 43 29
      jme3-core/src/main/java/com/jme3/light/LightProbe.java
  60. 11 9
      jme3-core/src/main/java/com/jme3/material/MatParam.java
  61. 10 3
      jme3-core/src/main/java/com/jme3/material/ShaderGenerationInfo.java
  62. 8 14
      jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
  63. 6 7
      jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java
  64. 7 10
      jme3-core/src/main/java/com/jme3/math/Transform.java
  65. 2 14
      jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java
  66. 2 5
      jme3-core/src/main/java/com/jme3/post/Filter.java
  67. 17 8
      jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java
  68. 12 10
      jme3-core/src/main/java/com/jme3/renderer/Camera.java
  69. 52 0
      jme3-core/src/main/java/com/jme3/renderer/Caps.java
  70. 1 2
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java
  71. 2 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL2.java
  72. 0 1
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java
  73. 0 6
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java
  74. 7 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugES.java
  75. 1 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java
  76. 1 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLFbo.java
  77. 24 9
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java
  78. 83 44
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  79. 14 10
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java
  80. 9 2
      jme3-core/src/main/java/com/jme3/renderer/opengl/TextureUtil.java
  81. 14 16
      jme3-core/src/main/java/com/jme3/scene/AssetLinkNode.java
  82. 102 129
      jme3-core/src/main/java/com/jme3/scene/BatchNode.java
  83. 123 122
      jme3-core/src/main/java/com/jme3/scene/CameraNode.java
  84. 3 0
      jme3-core/src/main/java/com/jme3/scene/Geometry.java
  85. 94 55
      jme3-core/src/main/java/com/jme3/scene/Mesh.java
  86. 29 24
      jme3-core/src/main/java/com/jme3/scene/control/LightControl.java
  87. 4 1
      jme3-core/src/main/java/com/jme3/scene/debug/Grid.java
  88. 11 9
      jme3-core/src/main/java/com/jme3/scene/debug/WireSphere.java
  89. 417 0
      jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java
  90. 238 0
      jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java
  91. 156 0
      jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java
  92. 218 0
      jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java
  93. 141 0
      jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java
  94. 7 9
      jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java
  95. 23 1
      jme3-core/src/main/java/com/jme3/scene/mesh/IndexBuffer.java
  96. 6 1
      jme3-core/src/main/java/com/jme3/scene/mesh/IndexByteBuffer.java
  97. 4 0
      jme3-core/src/main/java/com/jme3/scene/mesh/IndexIntBuffer.java
  98. 4 0
      jme3-core/src/main/java/com/jme3/scene/mesh/IndexShortBuffer.java
  99. 18 0
      jme3-core/src/main/java/com/jme3/scene/mesh/VirtualIndexBuffer.java
  100. 51 18
      jme3-core/src/main/java/com/jme3/shader/Glsl100ShaderGenerator.java

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@
 **/.classpath
 **/.settings
 **/.project
+**/out
 /.gradle/
 /.nb-gradle/
 /.idea/

+ 4 - 0
.travis.yml

@@ -1,5 +1,6 @@
 language: java
 sudo: false
+dist: precise
 
 branches:
   only:
@@ -18,6 +19,9 @@ matrix:
 
 addons:
   ssh_known_hosts: github.com
+  hosts:
+    - travisci
+  hostname: travisci
   apt:
     packages:
     - gcc-multilib

+ 8 - 1
appveyor.yml

@@ -23,7 +23,10 @@ environment:
     secure: Ek2lqC2e19qQDRRdlvnYyLFBq3TNj6YwKTAPuJ2VElJsxi9lQg+9ZP+VbP4kbHTx6Zaa++vtmOuxLZL7gdILrEEPa1Jix2BBLBfcxBUxe6w=
 
 install:
-  - cmd: del C:\Users\appveyor\.gradle\caches\modules-2\modules-2.lock
+- cmd: >-
+    set GRADLE_LOCK=C:\Users\appveyor\.gradle\caches\modules-2\modules-2.lock
+
+    if exist %GRADLE_LOCK% del %GRADLE_LOCK%
 
 build_script:
   - cmd: gradlew.bat -PbuildNativeProjects=true :jme3-bullet-native:assemble
@@ -40,6 +43,10 @@ on_success:
 - cmd: >-
     openssl aes-256-cbc -K %encrypted_f0a0b284e2e8_key% -iv %encrypted_f0a0b284e2e8_iv% -in private\key.enc -out c:\users\appveyor\.ssh\id_rsa -d
 
+    git config --global user.email "appveyor"
+
+    git config --global user.name "appveyor"
+
     git checkout -q %APPVEYOR_REPO_BRANCH%
 
     git add -- jme3-bullet-native/libs/native/windows/

+ 7 - 7
build.gradle

@@ -15,7 +15,7 @@ apply from: file('version.gradle')
 subprojects {
     if(!project.name.equals('jme3-android-examples')) {
         apply from: rootProject.file('common.gradle')
-        if (!['jme3-testdata', 'sdk'].contains(project.name)) {
+        if (!project.name.equals('jme3-testdata')) {
             apply from: rootProject.file('bintray.gradle')
         }
     } else {
@@ -66,9 +66,9 @@ task createZipDistribution(type:Zip,dependsOn:["dist","libDist"], description:"P
     archiveName "jME" + jmeFullVersion + ".zip"
     into("/") {
          from {"./dist"}
-    }            
+    }
     into("/sources") {
-        from {"$buildDir/libDist/sources"}     
+        from {"$buildDir/libDist/sources"}
     }
 }
 
@@ -88,14 +88,14 @@ task dist(dependsOn: [':jme3-examples:dist', 'mergedJavadoc']){
 task mergedJavadoc(type: Javadoc, description: 'Creates Javadoc from all the projects.') {
     title = 'jMonkeyEngine3'
     destinationDir = mkdir("dist/javadoc")
-    
+
     options.encoding = 'UTF-8'
 
     // Allows Javadoc to be generated on Java 8 despite doclint errors.
     if (JavaVersion.current().isJava8Compatible()) {
         options.addStringOption('Xdoclint:none', '-quiet')
     }
-    
+
     options.overview = file("javadoc-overview.html")
     // Note: The closures below are executed lazily.
     source subprojects.collect {project ->
@@ -116,7 +116,7 @@ task mergedSource(type: Copy){
 }
 
 task wrapper(type: Wrapper, description: 'Creates and deploys the Gradle wrapper to the current directory.') {
-    gradleVersion = '3.2.1'
+    gradleVersion = '4.1'
 }
 
 ext {
@@ -137,7 +137,7 @@ task configureAndroidNDK {
     if (System.env.ANDROID_NDK != null) {
         ndkBuildPath = System.env.ANDROID_NDK + File.separator + ndkBuildFile
     }
-    
+
     if (new File(ndkBuildPath).exists()) {
         ndkExists = true
         ndkCommandPath = ndkBuildPath

+ 3 - 0
common.gradle

@@ -16,6 +16,9 @@ repositories {
     maven {
         url "http://nifty-gui.sourceforge.net/nifty-maven-repo"
     }
+    flatDir {
+        dirs rootProject.file('lib')
+    }
 }
 
 dependencies {

+ 2 - 2
gradle.properties

@@ -19,8 +19,8 @@ buildAndroidExamples = false
 ndkPath = /opt/android-ndk-r10c
 
 # Path for downloading native Bullet
-bulletUrl = https://github.com/bulletphysics/bullet3/archive/2.83.7.zip
-bulletFolder = bullet3-2.83.7
+bulletUrl = https://github.com/bulletphysics/bullet3/archive/2.86.1.zip
+bulletFolder = bullet3-2.86.1
 bulletZipFile = bullet3.zip
 
 # POM settings

BIN
gradle/wrapper/gradle-wrapper.jar


+ 2 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,6 @@
-#Fri Nov 25 13:05:50 EST 2016
+#Sun Sep 17 22:55:30 EDT 2017
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip

+ 15 - 7
gradlew

@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
 
 ##############################################################################
 ##
@@ -154,11 +154,19 @@ if $cygwin ; then
     esac
 fi
 
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
-    JVM_OPTS=("$@")
+# Escape application args
+save ( ) {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
 }
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+APP_ARGS=$(save "$@")
 
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 0 - 6
gradlew.bat

@@ -49,7 +49,6 @@ goto fail
 @rem Get command-line arguments, handling Windows variants
 
 if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
 
 :win9xME_args
 @rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2
 if "x%~1" == "x" goto execute
 
 set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
 
 :execute
 @rem Setup the command line

+ 1 - 1
jme3-android/build.gradle

@@ -5,5 +5,5 @@ if (!hasProperty('mainClass')) {
 dependencies {
     compile project(':jme3-core')
     compile project(':jme3-plugins')
-    compile files('../lib/android.jar')
+    compileOnly 'android:android'
 }

+ 3 - 1
jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java

@@ -139,8 +139,10 @@ public class AndroidInputHandler14 extends AndroidInputHandler implements View.O
         boolean isJoystick =
                 ((source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
                 ((source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK);
+        boolean isUnknown =
+                (source & android.view.InputDevice.SOURCE_UNKNOWN) == android.view.InputDevice.SOURCE_UNKNOWN;
 
-        if (isTouch && touchInput != null) {
+        if (touchInput != null && (isTouch || (isUnknown && this.touchInput.isSimulateKeyboard()))) {
 //            logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}",
 //                    new Object[]{source, isTouch});
             consumed = touchInput.onKey(event);

+ 5 - 0
jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java

@@ -561,4 +561,9 @@ public class AndroidGL implements GL, GLExt, GLFbo {
     public void glBlendEquationSeparate(int colorMode, int alphaMode) {
         GLES20.glBlendEquationSeparate(colorMode, alphaMode);
     }
+    
+    @Override
+    public void glFramebufferTextureLayerEXT(int target, int attachment, int texture, int level, int layer) {
+        throw new UnsupportedOperationException("OpenGL ES 2 does not support texture arrays");
+    }
 }

+ 8 - 2
jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java

@@ -53,9 +53,11 @@ import com.jme3.input.dummy.DummyKeyInput;
 import com.jme3.input.dummy.DummyMouseInput;
 import com.jme3.renderer.android.AndroidGL;
 import com.jme3.renderer.opengl.GL;
+import com.jme3.renderer.opengl.GLDebugES;
 import com.jme3.renderer.opengl.GLExt;
 import com.jme3.renderer.opengl.GLFbo;
 import com.jme3.renderer.opengl.GLRenderer;
+import com.jme3.renderer.opengl.GLTracer;
 import com.jme3.system.*;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
@@ -195,8 +197,12 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
 
         timer = new NanoTimer();
         Object gl = new AndroidGL();
-        // gl = GLTracer.createGlesTracer((GL)gl, (GLExt)gl);
-        // gl = new GLDebugES((GL)gl, (GLExt)gl);
+        if (settings.getBoolean("GraphicsDebug")) {
+            gl = new GLDebugES((GL) gl, (GLExt) gl, (GLFbo) gl);
+        }
+        if (settings.getBoolean("GraphicsTrace")) {
+            gl = GLTracer.createGlesTracer(gl, GL.class, GLFbo.class, GLExt.class);
+        }
         renderer = new GLRenderer((GL)gl, (GLExt)gl, (GLFbo)gl);
         renderer.initialize();
 

+ 22 - 12
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java

@@ -1,6 +1,7 @@
 package com.jme3.scene.plugins.blender.materials;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -163,6 +164,12 @@ public final class MaterialContext implements Savable {
 
             material.setColor("Ambient", new ColorRGBA(ambientFactor, ambientFactor, ambientFactor, 1f));
         }
+        
+        // initializing unused "user-defined UV coords" to all available
+        Map<String, List<Vector2f>> unusedUserDefinedUVCoords = Collections.emptyMap();
+        if(userDefinedUVCoordinates != null && !userDefinedUVCoordinates.isEmpty()) {
+            unusedUserDefinedUVCoords = new HashMap<>(userDefinedUVCoordinates);
+        }
 
         // applying textures
         int textureIndex = 0;
@@ -175,16 +182,19 @@ public final class MaterialContext implements Savable {
                     String usedUserUVSet = combinedTexture.flatten(geometry, geometriesOMA, userDefinedUVCoordinates, blenderContext);
 
                     this.setTexture(material, combinedTexture.getMappingType(), combinedTexture.getResultTexture());
-                    List<Vector2f> uvs = combinedTexture.getResultUVS();
-                    if(uvs != null && uvs.size() > 0) {
-                        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);
-                    }//uvs might be null if the user assigned non existing UV coordinates group name to the mesh (this should be fixed in blender file)
                     
-                    if(usedUserUVSet != null) {
-                    	userDefinedUVCoordinates = new HashMap<>(userDefinedUVCoordinates);
-                    	userDefinedUVCoordinates.remove(usedUserUVSet);
+                    if(usedUserUVSet == null || unusedUserDefinedUVCoords.containsKey(usedUserUVSet)) {
+                        List<Vector2f> uvs = combinedTexture.getResultUVS();
+                        if(uvs != null && uvs.size() > 0) {
+                            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);
+                        }//uvs might be null if the user assigned non existing UV coordinates group name to the mesh (this should be fixed in blender file)
+
+                        // Remove used "user-defined UV coords" from the unused collection
+                        if(usedUserUVSet != null) {
+                	       unusedUserDefinedUVCoords.remove(usedUserUVSet);
+                        }
                     }
                 } 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);
@@ -192,12 +202,12 @@ public final class MaterialContext implements Savable {
             }
         }
 
-        if (userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) {
+        if (unusedUserDefinedUVCoords != null && unusedUserDefinedUVCoords.size() > 0) {
             LOGGER.fine("Storing unused, user defined UV coordinates sets.");
-            if (userDefinedUVCoordinates.size() > TextureHelper.TEXCOORD_TYPES.length) {
+            if (unusedUserDefinedUVCoords.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()) {
+            for (Entry<String, List<Vector2f>> entry : unusedUserDefinedUVCoords.entrySet()) {
                 if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) {
                     List<Vector2f> uvs = entry.getValue();
                     VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]);

+ 20 - 17
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java

@@ -291,27 +291,30 @@ public class MeshHelper extends AbstractBlenderHelper {
         	Structure defbase = (Structure) parent.getFieldValue("defbase");
             List<String> groupNames = new ArrayList<String>();
             List<Structure> defs = defbase.evaluateListBase();
-            for (Structure def : defs) {
-                groupNames.add(def.getFieldValue("name").toString());
-            }
+            
+            if(!defs.isEmpty()) {
+                for (Structure def : defs) {
+                    groupNames.add(def.getFieldValue("name").toString());
+                }
 
-            Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
-            if (pDvert.isNotNull()) {// assigning weights and bone indices
-                List<Structure> dverts = pDvert.fetchData();
-                for (Structure dvert : dverts) {
-                    Map<String, Float> weightsForVertex = new HashMap<String, Float>();
-                    Pointer pDW = (Pointer) dvert.getFieldValue("dw");
-                    if (pDW.isNotNull()) {
-                        List<Structure> dw = pDW.fetchData();
-                        for (Structure deformWeight : dw) {
-                            int groupIndex = ((Number) deformWeight.getFieldValue("def_nr")).intValue();
-                            float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
-                            String groupName = groupNames.get(groupIndex);
+                Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
+                if (pDvert.isNotNull()) {// assigning weights and bone indices
+                    List<Structure> dverts = pDvert.fetchData();
+                    for (Structure dvert : dverts) {
+                        Map<String, Float> weightsForVertex = new HashMap<String, Float>();
+                        Pointer pDW = (Pointer) dvert.getFieldValue("dw");
+                        if (pDW.isNotNull()) {
+                            List<Structure> dw = pDW.fetchData();
+                            for (Structure deformWeight : dw) {
+                                int groupIndex = ((Number) deformWeight.getFieldValue("def_nr")).intValue();
+                                float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
+                                String groupName = groupNames.get(groupIndex);
 
-                            weightsForVertex.put(groupName, weight);
+                                weightsForVertex.put(groupName, weight);
+                            }
                         }
+                        result.add(weightsForVertex);
                     }
-                    result.add(weightsForVertex);
                 }
             }
         }

+ 67 - 66
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ImageLoader.java

@@ -31,12 +31,14 @@
  */
 package com.jme3.scene.plugins.blender.textures;
 
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.TextureKey;
 import com.jme3.scene.plugins.blender.file.BlenderInputStream;
 import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
 import com.jme3.texture.plugins.AWTLoader;
-import com.jme3.texture.plugins.DDSLoader;
-import com.jme3.texture.plugins.TGALoader;
-import java.io.InputStream;
+import com.jme3.texture.plugins.HDRLoader;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
@@ -47,11 +49,29 @@ import java.util.logging.Logger;
  */
 /* package */class ImageLoader extends AWTLoader {
     private static final Logger LOGGER    = Logger.getLogger(ImageLoader.class.getName());
-
-    protected DDSLoader         ddsLoader = new DDSLoader();                              // DirectX image loader
+    private static final Logger hdrLogger = Logger.getLogger(HDRLoader.class.getName()); // Used to silence HDR Errors
+    
+    /**
+     * List of Blender-Supported Texture Extensions (we have to guess them, so
+     * the AssetLoader can find them. Not good, but better than nothing.
+     * Source: https://docs.blender.org/manual/en/dev/data_system/files/media/image_formats.html
+     */
+    private static final String[] extensions = new String[]
+        { /* Windows Bitmap */".bmp",
+          /* Iris */ ".sgi", ".rgb", ".bw",
+          /* PNG */ ".png",
+          /* JPEG */ ".jpg", ".jpeg",
+          /* JPEG 2000 */ ".jp2", ".j2c",
+          /* Targa */".tga",
+          /* Cineon & DPX */".cin", ".dpx",
+          /* OpenEXR */ ".exr",
+          /* Radiance HDR */ ".hdr",
+          /* TIFF */ ".tif", ".tiff",
+          /* DDS (Direct X) */ ".dds" };
 
     /**
-     * This method loads the image from the blender file itself. It tries each loader to load the image.
+     * This method loads a image which is packed into the blender file.
+     * It makes use of all the registered AssetLoaders
      * 
      * @param inputStream
      *            blender input stream
@@ -60,76 +80,57 @@ import java.util.logging.Logger;
      * @param flipY
      *            if the image should be flipped (does not work with DirectX image)
      * @return loaded image or null if it could not be loaded
+     * @deprecated This method has only been left in for API compability.
+     * Use loadTexture instead
      */
-    public Image loadImage(BlenderInputStream inputStream, int startPosition, boolean flipY) {
-        // loading using AWT loader
-        inputStream.setPosition(startPosition);
-        Image result = this.loadImage(inputStream, ImageType.AWT, flipY);
-        // loading using TGA loader
-        if (result == null) {
-            inputStream.setPosition(startPosition);
-            result = this.loadImage(inputStream, ImageType.TGA, flipY);
-        }
-        // loading using DDS loader
-        if (result == null) {
-            inputStream.setPosition(startPosition);
-            result = this.loadImage(inputStream, ImageType.DDS, flipY);
+    public Image loadImage(AssetManager assetManager, BlenderInputStream inputStream, int startPosition, boolean flipY) {
+        Texture tex = loadTexture(assetManager, inputStream, startPosition, flipY);
+        
+        if (tex == null) {
+            return null;
+        } else {
+            return tex.getImage();
         }
-
-        if (result == null) {
-            LOGGER.warning("Image could not be loaded by none of available loaders!");
-        }
-
-        return result;
     }
-
+    
     /**
-     * This method loads an image of a specified type from the given input stream.
+     * This method loads a texture which is packed into the blender file.
+     * It makes use of all the registered AssetLoaders
      * 
      * @param inputStream
-     *            the input stream we read the image from
-     * @param imageType
-     *            the type of the image {@link ImageType}
+     *            blender input stream
+     * @param startPosition
+     *            position in the stream where the image data starts
      * @param flipY
      *            if the image should be flipped (does not work with DirectX image)
-     * @return loaded image or null if it could not be loaded
+     * @return loaded texture or null if it could not be loaded
      */
-    public Image loadImage(InputStream inputStream, ImageType imageType, boolean flipY) {
-        Image result = null;
-        switch (imageType) {
-            case AWT:
-                try {
-                    result = this.load(inputStream, flipY);
-                } catch (Exception e) {
-                    LOGGER.warning("Unable to load image using AWT loader!");
-                }
-                break;
-            case DDS:
-                try {
-                    result = ddsLoader.load(inputStream);
-                } catch (Exception e) {
-                    LOGGER.warning("Unable to load image using DDS loader!");
-                }
-                break;
-            case TGA:
-                try {
-                    result = TGALoader.load(inputStream, flipY);
-                } catch (Exception e) {
-                    LOGGER.warning("Unable to load image using TGA loader!");
-                }
-                break;
-            default:
-                throw new IllegalStateException("Unknown image type: " + imageType);
+    public Texture loadTexture(AssetManager assetManager, BlenderInputStream inputStream, int startPosition, boolean flipY) {
+        inputStream.setPosition(startPosition);
+        TextureKey tKey;
+        Texture result = null;
+        
+        hdrLogger.setLevel(Level.SEVERE); // When we bruteforce try HDR on a non hdr file, it prints unreadable chars
+        
+        for (String ext: extensions) {
+            tKey = new TextureKey("dummy" + ext, flipY);
+            try {
+                result = assetManager.loadAssetFromStream(tKey, inputStream);
+            } catch (Exception e) {
+                continue;
+            }
+            
+            if (result != null) {
+                break; // Could locate a possible asset
+            }
+        }
+        
+        if (result == null) {
+            LOGGER.warning("Texture could not be loaded by any of the available loaders!\n"
+                    + "Since the file has been packed into the blender file, there is no"
+                    + "way for us to tell you which texture it was.");
         }
+        
         return result;
     }
-
-    /**
-     * Image types that can be loaded. AWT: png, jpg, jped or bmp TGA: tga DDS: DirectX image files
-     * 
-     * @author Marcin Roguski (Kaelthas)
-     */
-    private static enum ImageType {
-        AWT, TGA, DDS;
-    }
 }

+ 22 - 2
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TextureHelper.java

@@ -72,6 +72,7 @@ import com.jme3.texture.Texture.WrapMode;
 import com.jme3.texture.Texture2D;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.PlaceholderAssets;
 
 /**
  * A class that is used in texture calculations.
@@ -251,7 +252,11 @@ public class TextureHelper extends AbstractBlenderHelper {
                     blenderContext.getInputStream().setPosition(dataFileBlock.getBlockPosition());
 
                     // Should the texture be flipped? It works for sinbad ..
-                    result = new Texture2D(new ImageLoader().loadImage(blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true));
+                    result = new ImageLoader().loadTexture(blenderContext.getAssetManager(), blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true);
+                    if (result == null) {
+                        result = new Texture2D(PlaceholderAssets.getPlaceholderImage(blenderContext.getAssetManager()));
+                        LOGGER.fine("ImageLoader returned null. It probably failed to load the packed texture, using placeholder asset");
+                    }
                 }
             }
         //} else {
@@ -614,8 +619,15 @@ public class TextureHelper extends AbstractBlenderHelper {
                     int texflag = ((Number) textureData.mtex.getFieldValue("texflag")).intValue();
                     boolean negateTexture = (texflag & 0x04) != 0;
 
+                    boolean colorSet = false;
                     for (int i = 0; i < mappings.length; ++i) {
                         if ((mappings[i] & mapto.intValue()) != 0) {
+                            if(mappings[i] == MaterialContext.MTEX_COL) {
+                                colorSet = true;
+                            } else if(colorSet && mappings[i] == MaterialContext.MTEX_ALPHA) {
+                                continue;
+                            }
+                            
                             CombinedTexture combinedTexture = new CombinedTexture(mappings[i], !skyTexture);
                             int blendType = ((Number) textureData.mtex.getFieldValue("blendtype")).intValue();
                             float[] color = new float[] { ((Number) textureData.mtex.getFieldValue("r")).floatValue(), ((Number) textureData.mtex.getFieldValue("g")).floatValue(), ((Number) textureData.mtex.getFieldValue("b")).floatValue() };
@@ -646,8 +658,16 @@ public class TextureHelper extends AbstractBlenderHelper {
         Map<Number, List<TextureData>> result = new HashMap<Number, List<TextureData>>();
         for (TextureData data : textures) {
             Number mapto = (Number) data.mtex.getFieldValue("mapto");
+            
+            boolean colorSet = false;
             for (int i = 0; i < mappings.length; ++i) {
                 if ((mappings[i] & mapto.intValue()) != 0) {
+                    if(mappings[i] == MaterialContext.MTEX_COL) {
+                        colorSet = true;
+                    } else if(colorSet && mappings[i] == MaterialContext.MTEX_ALPHA) {
+                        continue;
+                    }
+                    
                     List<TextureData> datas = result.get(mappings[i]);
                     if (datas == null) {
                         datas = new ArrayList<TextureData>();
@@ -668,4 +688,4 @@ public class TextureHelper extends AbstractBlenderHelper {
         /** The name of the user's UV coordinates that are used for this texture. */
         public String    uvCoordinatesName;
     }
-}
+}

+ 11 - 1
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderAWT.java

@@ -163,9 +163,19 @@ public class TextureBlenderAWT extends AbstractTextureBlender {
      *            the blender context
      */
     protected void blendPixel(float[] result, float[] materialColor, float[] pixelColor, BlenderContext blenderContext) {
+        // We calculate first the importance of the texture (colFactor * texAlphaValue)
         float blendFactor = this.blendFactor * pixelColor[3];
-        float oneMinusFactor = 1.0f - blendFactor, col;
+        // Then, we get the object material factor ((1 - texture importance) * matAlphaValue) 
+        float oneMinusFactor = (1f - blendFactor) * materialColor[3];
+        // Finally, we can get the final blendFactor, which is 1 - the material factor.
+        blendFactor = 1f - oneMinusFactor;
+        
+        // --- Compact method ---
+        // float blendFactor = this.blendFactor * (1f - ((1f - pixelColor[3]) * materialColor[3]));
+        // float oneMinusFactor = 1f - blendFactor;
 
+        float col;
+        
         switch (blendType) {
             case MTEX_BLEND:
                 result[0] = blendFactor * pixelColor[0] + oneMinusFactor * materialColor[0];

BIN
jme3-bullet-native/libs/native/linux/x86/libbulletjme.so


BIN
jme3-bullet-native/libs/native/linux/x86_64/libbulletjme.so


BIN
jme3-bullet-native/libs/native/osx/x86/libbulletjme.dylib


BIN
jme3-bullet-native/libs/native/osx/x86_64/libbulletjme.dylib


BIN
jme3-bullet-native/libs/native/windows/x86/bulletjme.dll


BIN
jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll


+ 1 - 1
jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_PhysicsCollisionEvent.cpp

@@ -209,7 +209,7 @@ JNIEXPORT jboolean JNICALL Java_com_jme3_bullet_collision_PhysicsCollisionEvent_
         env->ThrowNew(newExc, "The manifoldPoint does not exist.");
         return 0;
     }
-    return mp -> m_lateralFrictionInitialized;
+    return (mp -> m_contactPointFlags) &  BT_CONTACT_FLAG_LATERAL_FRICTION_INITIALIZED;
 }
 
 /*

+ 252 - 13
jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsCharacter.cpp

@@ -129,20 +129,100 @@ extern "C" {
 
     /*
      * Class:     com_jme3_bullet_objects_PhysicsCharacter
-     * Method:    setUpAxis
-     * Signature: (JI)V
+     * Method:    setUp
+     * Signature: (JLcom/jme3/math/Vector3f;)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setUp
+    (JNIEnv *env, jobject object, jlong objectId, jobject value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        btVector3 vec = btVector3();
+        jmeBulletUtil::convert(env,  value, &vec);
+        character->setUp(vec);
+    }
+
+     /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    setAngularVelocity
+     * Signature: (JLcom/jme3/math/Vector3f;)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setAngularVelocity
+    (JNIEnv *env, jobject object, jlong objectId, jobject value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        btVector3 vec = btVector3();
+        jmeBulletUtil::convert(env, value, &vec);
+        character->setAngularVelocity(vec);
+    }
+
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    getAngularVelocity
+     * Signature: (JLcom/jme3/math/Vector3f;)V
+     */
+     
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getAngularVelocity
+    (JNIEnv *env, jobject object, jlong objectId, jobject value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        btVector3 a_vel = character->getAngularVelocity();
+        jmeBulletUtil::convert(env, &a_vel, value);
+    }
+
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    setLinearVelocity
+     * Signature: (JLcom/jme3/math/Vector3f;)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setLinearVelocity
+    (JNIEnv *env, jobject object, jlong objectId, jobject value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        btVector3 vec = btVector3();
+        jmeBulletUtil::convert(env, value, &vec);
+        character->setLinearVelocity(vec);
+    }
+
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    getLinearVelocity
+     * Signature: (JLcom/jme3/math/Vector3f;)V
      */
-    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setUpAxis
-    (JNIEnv *env, jobject object, jlong objectId, jint value) {
+     
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getLinearVelocity
+    (JNIEnv *env, jobject object, jlong objectId, jobject value) {
         btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
         if (character == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The native object does not exist.");
             return;
         }
-        character->setUpAxis(value);
+        btVector3 l_vel = character->getLinearVelocity();
+        jmeBulletUtil::convert(env, &l_vel, value);
     }
 
+
+
+
     /*
      * Class:     com_jme3_bullet_objects_PhysicsCharacter
      * Method:    setFallSpeed
@@ -178,25 +258,130 @@ extern "C" {
     /*
      * Class:     com_jme3_bullet_objects_PhysicsCharacter
      * Method:    setGravity
-     * Signature: (JF)V
+     * Signature:  (JLcom/jme3/math/Vector3f;)V
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setGravity
-    (JNIEnv *env, jobject object, jlong objectId, jfloat value) {
+    (JNIEnv *env, jobject object, jlong objectId, jobject value) {
         btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
         if (character == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The native object does not exist.");
             return;
         }
-        character->setGravity(value);
+
+        btVector3 vec = btVector3();
+        jmeBulletUtil::convert(env, value, &vec);
+        character->setGravity(vec);
     }
 
     /*
      * Class:     com_jme3_bullet_objects_PhysicsCharacter
      * Method:    getGravity
+     * Signature:  (JLcom/jme3/math/Vector3f;)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getGravity
+    (JNIEnv *env, jobject object, jlong objectId,jobject value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        btVector3 g = character->getGravity();
+        jmeBulletUtil::convert(env, &g, value);
+    }
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    setLinearDamping
+     * Signature: (JF)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setLinearDamping
+    (JNIEnv *env, jobject object, jlong objectId,jfloat value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return ;
+        }
+        character->setLinearDamping(value);
+    }
+
+
+   /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    getLinearDamping
+     * Signature: (J)F
+     */
+    JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getLinearDamping
+    (JNIEnv *env, jobject object, jlong objectId) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return 0;
+        }
+        return character->getLinearDamping();
+    }
+
+
+      /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    setAngularDamping
+     * Signature: (JF)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setAngularDamping
+    (JNIEnv *env, jobject object, jlong objectId,jfloat value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        character->setAngularDamping(value);
+    }
+
+
+   /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    getAngularDamping
+     * Signature: (J)F
+     */
+    JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getAngularDamping
+    (JNIEnv *env, jobject object, jlong objectId) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return 0;
+        }
+        return character->getAngularDamping();
+    }
+
+
+        /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    setStepHeight
+     * Signature: (JF)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setStepHeight
+    (JNIEnv *env, jobject object, jlong objectId,jfloat value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        character->setStepHeight(value);
+    }
+
+
+   /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    getStepHeight
      * Signature: (J)F
      */
-    JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getGravity
+    JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getStepHeight
     (JNIEnv *env, jobject object, jlong objectId) {
         btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
         if (character == NULL) {
@@ -204,9 +389,10 @@ extern "C" {
             env->ThrowNew(newExc, "The native object does not exist.");
             return 0;
         }
-        return character->getGravity();
+        return character->getStepHeight();
     }
 
+
     /*
      * Class:     com_jme3_bullet_objects_PhysicsCharacter
      * Method:    setMaxSlope
@@ -239,6 +425,39 @@ extern "C" {
         return character->getMaxSlope();
     }
 
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    setMaxPenetrationDepth
+     * Signature: (JF)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setMaxPenetrationDepth
+    (JNIEnv *env, jobject object, jlong objectId, jfloat value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        character->setMaxPenetrationDepth(value);
+    }
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    getMaxPenetrationDepth
+     * Signature: (J)F
+     */
+    JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getMaxPenetrationDepth
+    (JNIEnv *env, jobject object, jlong objectId) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return 0;
+        }
+        return character->getMaxPenetrationDepth();
+    }
+
     /*
      * Class:     com_jme3_bullet_objects_PhysicsCharacter
      * Method:    onGround
@@ -258,17 +477,37 @@ extern "C" {
     /*
      * Class:     com_jme3_bullet_objects_PhysicsCharacter
      * Method:    jump
-     * Signature: (J)V
+     * Signature: (JLcom/jme3/math/Vector3f;)V
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_jump
-    (JNIEnv *env, jobject object, jlong objectId) {
+    (JNIEnv *env, jobject object, jlong objectId,jobject value) {
         btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
         if (character == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The native object does not exist.");
             return;
         }
-        character->jump();
+        btVector3 vec = btVector3();
+        jmeBulletUtil::convert(env, value, &vec);
+        character->jump(vec);
+    }
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsCharacter
+     * Method:    applyImpulse
+     * Signature: (JLcom/jme3/math/Vector3f;)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_applyImpulse
+    (JNIEnv *env, jobject object, jlong objectId,jobject value) {
+        btKinematicCharacterController* character = reinterpret_cast<btKinematicCharacterController*>(objectId);
+        if (character == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        btVector3 vec = btVector3();
+        jmeBulletUtil::convert(env, value, &vec);
+        character->applyImpulse(vec);
     }
 
     /*

+ 127 - 11
jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsCharacter.h

@@ -83,11 +83,47 @@ JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setWalkDire
 
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter
- * Method:    setUpAxis
- * Signature: (JI)V
+ * Method:    setUp
+ * Signature: (JLcom/jme3/math/Vector3f;)V
  */
-JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setUpAxis
-  (JNIEnv *, jobject, jlong, jint);
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setUp
+    (JNIEnv *, jobject , jlong , jobject );
+
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    setAngularVelocity
+  * Signature: (JLcom/jme3/math/Vector3f;)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setAngularVelocity
+    (JNIEnv *, jobject , jlong , jobject ) ;
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    getAngularVelocity
+  * Signature: (JLcom/jme3/math/Vector3f;)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getAngularVelocity
+   (JNIEnv *, jobject , jlong , jobject );
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    setLinearVelocity
+  * Signature: (JLcom/jme3/math/Vector3f;)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setLinearVelocity
+    (JNIEnv *, jobject , jlong , jobject );
+
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    getLinearVelocity
+  * Signature: (JLcom/jme3/math/Vector3f;)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getLinearVelocity
+   (JNIEnv *, jobject , jlong , jobject );
+
+
 
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter
@@ -108,18 +144,72 @@ JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setJumpSpee
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter
  * Method:    setGravity
- * Signature: (JF)V
+ * Signature:  (JLcom/jme3/math/Vector3f;)V
  */
 JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setGravity
-  (JNIEnv *, jobject, jlong, jfloat);
+  (JNIEnv *, jobject, jlong, jobject);
 
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter
  * Method:    getGravity
- * Signature: (J)F
+ * Signature:  (JLcom/jme3/math/Vector3f;)V
  */
-JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getGravity
-  (JNIEnv *, jobject, jlong);
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getGravity
+  (JNIEnv *, jobject, jlong, jobject);
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    setLinearDamping
+  * Signature: (JF)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setLinearDamping
+   (JNIEnv *, jobject , jlong ,jfloat );
+
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    getLinearDamping
+  * Signature: (J)F
+  */
+JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getLinearDamping
+    (JNIEnv *, jobject , jlong );
+
+
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    setAngularDamping
+  * Signature: (JF)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setAngularDamping
+    (JNIEnv *, jobject , jlong ,jfloat );
+
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    getAngularDamping
+  * Signature: (J)F
+  */
+JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getAngularDamping
+    (JNIEnv *, jobject , jlong );
+
+  
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    setStepHeight
+  * Signature: (JF)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setStepHeight
+    (JNIEnv *, jobject , jlong ,jfloat );
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    getStepHeight
+  * Signature: (J)F
+  */
+JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getStepHeight
+    (JNIEnv *, jobject , jlong );
+
 
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter
@@ -137,6 +227,24 @@ JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setMaxSlope
 JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getMaxSlope
   (JNIEnv *, jobject, jlong);
 
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    setMaxPenetrationDepth
+  * Signature: (JF)V
+  */
+  JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_setMaxPenetrationDepth
+    (JNIEnv *, jobject , jlong , jfloat );
+
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    getMaxPenetrationDepth
+  * Signature: (J)F
+  */
+JNIEXPORT jfloat JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_getMaxPenetrationDepth
+    (JNIEnv *, jobject , jlong );
+
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter
  * Method:    onGround
@@ -148,10 +256,18 @@ JNIEXPORT jboolean JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_onGroun
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter
  * Method:    jump
- * Signature: (J)V
+ * Signature: (JLcom/jme3/math/Vector3f;)V
  */
 JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_jump
-  (JNIEnv *, jobject, jlong);
+  (JNIEnv *, jobject, jlong, jobject);
+
+/*
+  * Class:     com_jme3_bullet_objects_PhysicsCharacter
+  * Method:    applyImpulse
+  * Signature: (JLcom/jme3/math/Vector3f;)V
+  */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsCharacter_applyImpulse
+    (JNIEnv *, jobject , jlong ,jobject );
 
 /*
  * Class:     com_jme3_bullet_objects_PhysicsCharacter

+ 37 - 1
jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsRigidBody.cpp

@@ -151,6 +151,41 @@ extern "C" {
         //        }
     }
 
+
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsRigidBody
+     * Method:    setInverseInertiaLocal
+     * Signature: (JLcom/jme3/math/Vector3f;)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsRigidBody_setInverseInertiaLocal
+    (JNIEnv *env, jobject object, jlong bodyId, jobject value) {
+        btRigidBody* body = reinterpret_cast<btRigidBody*>(bodyId);
+        if (body == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        btVector3 vec = btVector3();
+        jmeBulletUtil::convert(env, value, &vec);
+        body->setInvInertiaDiagLocal(vec);
+    }
+    
+    /*
+     * Class:     com_jme3_bullet_objects_PhysicsRigidBody
+     * Method:    getInverseInertiaLocal
+     * Signature: (JLcom/jme3/math/Vector3f;)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsRigidBody_getInverseInertiaLocal
+    (JNIEnv *env, jobject object, jlong bodyId, jobject value) {
+        btRigidBody* body = reinterpret_cast<btRigidBody*>(bodyId);
+        if (body == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+        jmeBulletUtil::convert(env, &body->getInvInertiaDiagLocal(), value);
+    }
+
     /*
      * Class:     com_jme3_bullet_objects_PhysicsRigidBody
      * Method:    getPhysicsLocation
@@ -217,7 +252,8 @@ extern "C" {
             body->setActivationState(DISABLE_DEACTIVATION);
         } else {
             body->setCollisionFlags(body->getCollisionFlags() & ~btCollisionObject::CF_KINEMATIC_OBJECT);
-            body->setActivationState(ACTIVE_TAG);
+	    body->activate(true);
+	    body->forceActivationState(ACTIVE_TAG);
         }
     }
 

+ 16 - 0
jme3-bullet-native/src/native/cpp/com_jme3_bullet_objects_PhysicsRigidBody.h

@@ -89,6 +89,22 @@ JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsRigidBody_setPhysicsR
 JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsRigidBody_getPhysicsLocation
   (JNIEnv *, jobject, jlong, jobject);
 
+/*
+ * Class:     com_jme3_bullet_objects_PhysicsRigidBody
+ * Method:    setInverseInertiaLocal
+ * Signature: (JLcom/jme3/math/Vector3f;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsRigidBody_setInverseInertiaLocal
+  (JNIEnv *, jobject, jlong, jobject);
+
+/*
+ * Class:     com_jme3_bullet_objects_PhysicsRigidBody
+ * Method:    getInverseInertiaLocal
+ * Signature: (JLcom/jme3/math/Vector3f;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_bullet_objects_PhysicsRigidBody_getInverseInertiaLocal
+  (JNIEnv *, jobject, jlong, jobject);
+
 /*
  * Class:     com_jme3_bullet_objects_PhysicsRigidBody
  * Method:    getPhysicsRotation

+ 152 - 13
jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsCharacter.java

@@ -127,13 +127,65 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
     public Vector3f getWalkDirection() {
         return walkDirection;
     }
+    
+    /**
+     * @deprecated Deprecated in bullet 2.86.1 use setUp(Vector3f) instead
+     */
+    @Deprecated
+	public void setUpAxis(int axis) {
+		if(axis<0) axis=0;
+		else if(axis>2) axis=2;
+		switch(axis){
+			case 0:
+				setUp(Vector3f.UNIT_X);
+				break;
+			case 1:
+				setUp(Vector3f.UNIT_Y);
+				break;
+			case 2:
+				setUp(Vector3f.UNIT_Z);
+		}
+	}
+    
+    public void setUp(Vector3f axis) {
+        setUp(characterId, axis);
+    }
+
+
+    private native void setUp(long characterId, Vector3f axis);
+
+    
+    public void setAngularVelocity(Vector3f v){
+    	setAngularVelocity(characterId,v);
+    }
+        
+    private native void setAngularVelocity(long characterId, Vector3f v);
+
+
+    public Vector3f getAngularVelocity(Vector3f out){
+    	if(out==null)out=new Vector3f();
+    	getAngularVelocity(characterId,out);
+    	return out;
+    }
+    
+    private native void getAngularVelocity(long characterId, Vector3f out);
+    
 
-    public void setUpAxis(int axis) {
-        upAxis = axis;
-        setUpAxis(characterId, axis);
+    public void setLinearVelocity(Vector3f v){
+    	setLinearVelocity(characterId,v);
     }
+        
+    private native void setLinearVelocity(long characterId, Vector3f v);
+
 
-    private native void setUpAxis(long characterId, int axis);
+    public Vector3f getLinearVelocity(Vector3f out){
+    	if(out==null)out=new Vector3f();
+    	getLinearVelocity(characterId,out);
+    	return out;
+    }
+    
+    private native void getLinearVelocity(long characterId, Vector3f out);
+        
 
     public int getUpAxis() {
         return upAxis;
@@ -161,18 +213,96 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
         return jumpSpeed;
     }
 
+    /**
+     * @deprecated Deprecated in bullet 2.86.1. Use setGravity(Vector3f) instead.
+     */
+    @Deprecated
     public void setGravity(float value) {
+    	setGravity(new Vector3f(0,value,0));
+    }
+    
+    public void setGravity(Vector3f value) {
         setGravity(characterId, value);
     }
 
-    private native void setGravity(long characterId, float gravity);
+    private native void setGravity(long characterId, Vector3f gravity);
 
+    /**
+     * @deprecated Deprecated in bullet 2.86.1. Use getGravity(Vector3f) instead.
+     */
+    @Deprecated
     public float getGravity() {
-        return getGravity(characterId);
-    }
-
-    private native float getGravity(long characterId);
-
+        return getGravity(null).y;
+    }
+
+    public Vector3f getGravity(Vector3f out) {
+    	if(out==null)out=new Vector3f();
+    	getGravity(characterId,out);
+    	return out;
+    }
+
+    private native void getGravity(long characterId,Vector3f out);
+
+        
+    public float getLinearDamping(){
+    	return getLinearDamping(characterId);
+    }
+    
+    private native float getLinearDamping(long characterId);
+    
+        
+    public void setLinearDamping(float v ){
+    	setLinearDamping(characterId,v );
+    }
+    
+    private native void setLinearDamping(long characterId,float v);
+    
+    
+    public float getAngularDamping(){
+    	return getAngularDamping(characterId);
+    }
+    
+    private native float getAngularDamping(long characterId);
+    
+        
+    public void setAngularDamping(float v ){
+    	setAngularDamping(characterId,v );
+    }
+    
+    private native void setAngularDamping(long characterId,float v);
+    
+    
+    public float getStepHeight(){
+    	return getStepHeight(characterId);
+    }
+    
+    private native float getStepHeight(long characterId);
+    
+        
+    public void setStepHeight(float v ){
+    	setStepHeight(characterId,v );
+    }
+    
+    private native void setStepHeight(long characterId,float v);
+    
+    
+    public float getMaxPenetrationDepth(){
+    	return getMaxPenetrationDepth(characterId);
+    }
+    
+    private native float getMaxPenetrationDepth(long characterId);
+    
+        
+    public void setMaxPenetrationDepth(float v ){
+    	setMaxPenetrationDepth(characterId,v );
+    }
+    
+    private native void setMaxPenetrationDepth(long characterId,float v);
+    
+
+    
+    
+    
     public void setMaxSlope(float slopeRadians) {
         setMaxSlope(characterId, slopeRadians);
     }
@@ -191,11 +321,20 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
 
     private native boolean onGround(long characterId);
 
+    /**
+     * @deprecated Deprecated in bullet 2.86.1. Use jump(Vector3f) instead.
+     */
+    @Deprecated
     public void jump() {
-        jump(characterId);
+    	jump(Vector3f.UNIT_Y);
     }
-
-    private native void jump(long characterId);
+    
+    
+    public void jump(Vector3f dir) {
+    	jump(characterId,dir);
+    }
+    
+    private native void jump(long characterId,Vector3f v);
 
     @Override
     public void setCollisionShape(CollisionShape collisionShape) {

+ 15 - 1
jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsRigidBody.java

@@ -185,6 +185,21 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
         getPhysicsRotation(objectId, rot);
         return rot;
     }
+    
+    public void setInverseInertiaLocal(Vector3f gravity) {
+    	setInverseInertiaLocal(objectId, gravity);
+    }
+    private native void setInverseInertiaLocal(long objectId, Vector3f gravity);
+    
+    public Vector3f getInverseInertiaLocal(Vector3f trans) {
+        if (trans == null) {
+            trans = new Vector3f();
+        }
+        getInverseInertiaLocal(objectId, trans);
+        return trans;
+    }
+    
+    private native void getInverseInertiaLocal(long objectId, Vector3f vector);
 
     private native void getPhysicsRotation(long objectId, Quaternion rot);
 
@@ -352,7 +367,6 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
     public void setGravity(Vector3f gravity) {
         setGravity(objectId, gravity);
     }
-
     private native void setGravity(long objectId, Vector3f gravity);
 
     public float getFriction() {

+ 10 - 2
jme3-bullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2017 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -35,6 +35,7 @@ import com.jme3.bullet.collision.shapes.CollisionShape;
 import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
 import com.jme3.bullet.collision.shapes.infos.ChildCollisionShape;
 import com.jme3.math.Matrix3f;
+import com.jme3.math.Vector3f;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.Node;
@@ -111,9 +112,16 @@ public class DebugShapeFactory {
 
     public static Mesh getDebugMesh(CollisionShape shape) {
         Mesh mesh = new Mesh();
-        mesh = new Mesh();
         DebugMeshCallback callback = new DebugMeshCallback();
+        /*
+         * Populate the mesh based on an unscaled shape;
+         * the shape's scale will be applied later, to the geometry.
+         */
+        Vector3f savedScale = shape.getScale().clone();
+        shape.setScale(Vector3f.UNIT_XYZ);
         getVertices(shape.getObjectId(), callback);
+        shape.setScale(savedScale);
+
         mesh.setBuffer(Type.Position, 3, callback.getVertices());
         mesh.getFloatBuffer(Type.Position).clear();
         return mesh;

+ 369 - 365
jme3-core/src/main/java/com/jme3/animation/AnimChannel.java

@@ -1,365 +1,369 @@
-/*
- * 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.animation;
-
-import com.jme3.math.FastMath;
-import com.jme3.util.TempVars;
-import java.util.BitSet;
-
-/**
- * <code>AnimChannel</code> provides controls, such as play, pause,
- * fast forward, etc, for an animation. The animation
- * channel may influence the entire model or specific bones of the model's
- * skeleton. A single model may have multiple animation channels influencing
- * various parts of its body. For example, a character model may have an
- * animation channel for its feet, and another for its torso, and
- * the animations for each channel are controlled independently.
- * 
- * @author Kirill Vainer
- */
-public final class AnimChannel {
-
-    private static final float DEFAULT_BLEND_TIME = 0.15f;
-    
-    private AnimControl control;
-
-    private BitSet affectedBones;
-
-    private Animation animation;
-    private Animation blendFrom;
-    private float time;
-    private float speed;
-    private float timeBlendFrom;
-    private float blendTime;
-    private float speedBlendFrom;
-    private boolean notified=false;
-
-    private LoopMode loopMode, loopModeBlendFrom;
-    
-    private float blendAmount = 1f;
-    private float blendRate   = 0;
-    
-    AnimChannel(AnimControl control){
-        this.control = control;
-    }
-
-    /**
-     * Returns the parent control of this AnimChannel.
-     * 
-     * @return the parent control of this AnimChannel.
-     * @see AnimControl
-     */
-    public AnimControl getControl() {
-        return control;
-    }
-    
-    /**
-     * @return The name of the currently playing animation, or null if
-     * none is assigned.
-     *
-     * @see AnimChannel#setAnim(java.lang.String) 
-     */
-    public String getAnimationName() {
-        return animation != null ? animation.getName() : null;
-    }
-
-    /**
-     * @return The loop mode currently set for the animation. The loop mode
-     * determines what will happen to the animation once it finishes
-     * playing.
-     * 
-     * For more information, see the LoopMode enum class.
-     * @see LoopMode
-     * @see AnimChannel#setLoopMode(com.jme3.animation.LoopMode)
-     */
-    public LoopMode getLoopMode() {
-        return loopMode;
-    }
-
-    /**
-     * @param loopMode Set the loop mode for the channel. The loop mode
-     * determines what will happen to the animation once it finishes
-     * playing.
-     *
-     * For more information, see the LoopMode enum class.
-     * @see LoopMode
-     */
-    public void setLoopMode(LoopMode loopMode) {
-        this.loopMode = loopMode;
-    }
-
-    /**
-     * @return The speed that is assigned to the animation channel. The speed
-     * is a scale value starting from 0.0, at 1.0 the animation will play
-     * at its default speed.
-     *
-     * @see AnimChannel#setSpeed(float)
-     */
-    public float getSpeed() {
-        return speed;
-    }
-
-    /**
-     * @param speed Set the speed of the animation channel. The speed
-     * is a scale value starting from 0.0, at 1.0 the animation will play
-     * at its default speed.
-     */
-    public void setSpeed(float speed) {
-        this.speed = speed;
-        if(blendTime>0){
-            this.speedBlendFrom = speed;
-            blendTime = Math.min(blendTime, animation.getLength() / speed);  
-            blendRate = 1/ blendTime;
-        }
-    }
-
-    /**
-     * @return The time of the currently playing animation. The time
-     * starts at 0 and continues on until getAnimMaxTime().
-     *
-     * @see AnimChannel#setTime(float)
-     */
-    public float getTime() {
-        return time;
-    }
-
-    /**
-     * @param time Set the time of the currently playing animation, the time
-     * is clamped from 0 to {@link #getAnimMaxTime()}. 
-     */
-    public void setTime(float time) {
-        this.time = FastMath.clamp(time, 0, getAnimMaxTime());
-    }
-
-    /**
-     * @return The length of the currently playing animation, or zero
-     * if no animation is playing.
-     *
-     * @see AnimChannel#getTime()
-     */
-    public float getAnimMaxTime(){
-        return animation != null ? animation.getLength() : 0f;
-    }
-
-    /**
-     * Set the current animation that is played by this AnimChannel.
-     * <p>
-     * This resets the time to zero, and optionally blends the animation
-     * over <code>blendTime</code> seconds with the currently playing animation.
-     * Notice that this method will reset the control's speed to 1.0.
-     *
-     * @param name The name of the animation to play
-     * @param blendTime The blend time over which to blend the new animation
-     * with the old one. If zero, then no blending will occur and the new
-     * animation will be applied instantly.
-     */
-    public void setAnim(String name, float blendTime){
-        if (name == null)
-            throw new IllegalArgumentException("name cannot be null");
-
-        if (blendTime < 0f)
-            throw new IllegalArgumentException("blendTime cannot be less than zero");
-
-        Animation anim = control.animationMap.get(name);
-        if (anim == null)
-            throw new IllegalArgumentException("Cannot find animation named: '"+name+"'");
-
-        control.notifyAnimChange(this, name);
-
-        if (animation != null && blendTime > 0f){
-            this.blendTime = blendTime;
-            // activate blending
-            blendTime = Math.min(blendTime, anim.getLength() / speed);            
-            blendFrom = animation;
-            timeBlendFrom = time;
-            speedBlendFrom = speed;
-            loopModeBlendFrom = loopMode;
-            blendAmount = 0f;
-            blendRate   = 1f / blendTime;
-        }else{
-            blendFrom = null;
-        }
-
-        animation = anim;
-        time = 0;
-        speed = 1f;
-        loopMode = LoopMode.Loop;
-        notified = false;
-    }
-
-    /**
-     * Set the current animation that is played by this AnimChannel.
-     * <p>
-     * See {@link #setAnim(java.lang.String, float)}.
-     * The blendTime argument by default is 150 milliseconds.
-     * 
-     * @param name The name of the animation to play
-     */
-    public void setAnim(String name){
-        setAnim(name, DEFAULT_BLEND_TIME);
-    }
-
-    /**
-     * Add all the bones of the model's skeleton to be
-     * influenced by this animation channel.
-     */
-    public void addAllBones() {
-        affectedBones = null;
-    }
-
-    /**
-     * Add a single bone to be influenced by this animation channel.
-     */
-    public void addBone(String name) {
-        addBone(control.getSkeleton().getBone(name));
-    }
-
-    /**
-     * Add a single bone to be influenced by this animation channel.
-     */
-    public void addBone(Bone bone) {
-        int boneIndex = control.getSkeleton().getBoneIndex(bone);
-        if(affectedBones == null) {
-            affectedBones = new BitSet(control.getSkeleton().getBoneCount());
-        }
-        affectedBones.set(boneIndex);
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel starting from the
-     * given bone name and going toward the root bone.
-     */
-    public void addToRootBone(String name) {
-        addToRootBone(control.getSkeleton().getBone(name));
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel starting from the
-     * given bone and going toward the root bone.
-     */
-    public void addToRootBone(Bone bone) {
-        addBone(bone);
-        while (bone.getParent() != null) {
-            bone = bone.getParent();
-            addBone(bone);
-        }
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel, starting
-     * from the given named bone and going toward its children.
-     */
-    public void addFromRootBone(String name) {
-        addFromRootBone(control.getSkeleton().getBone(name));
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel, starting
-     * from the given bone and going toward its children.
-     */
-    public void addFromRootBone(Bone bone) {
-        addBone(bone);
-        if (bone.getChildren() == null)
-            return;
-        for (Bone childBone : bone.getChildren()) {
-            addBone(childBone);
-            addFromRootBone(childBone);
-        }
-    }
-
-    BitSet getAffectedBones(){
-        return affectedBones;
-    }
-    
-    public void reset(boolean rewind){
-        if(rewind){
-            setTime(0);        
-            if(control.getSkeleton()!=null){
-                control.getSkeleton().resetAndUpdate();
-            }else{
-                TempVars vars = TempVars.get();
-                update(0, vars);
-                vars.release();    
-            }
-        }
-        animation = null;
-        notified = false;
-    }
-
-    void update(float tpf, TempVars vars) {
-        if (animation == null)
-            return;
-
-        if (blendFrom != null && blendAmount != 1.0f){
-            // The blendFrom anim is set, the actual animation
-            // playing will be set 
-//            blendFrom.setTime(timeBlendFrom, 1f, control, this, vars);
-            blendFrom.setTime(timeBlendFrom, 1f - blendAmount, control, this, vars);
-            
-            timeBlendFrom += tpf * speedBlendFrom;
-            timeBlendFrom = AnimationUtils.clampWrapTime(timeBlendFrom,
-                                          blendFrom.getLength(),
-                                          loopModeBlendFrom);
-            if (timeBlendFrom < 0){
-                timeBlendFrom = -timeBlendFrom;
-                speedBlendFrom = -speedBlendFrom;
-            }
-
-            blendAmount += tpf * blendRate;
-            if (blendAmount > 1f){
-                blendAmount = 1f;
-                blendFrom = null;
-            }
-        }
-        
-        animation.setTime(time, blendAmount, control, this, vars);
-        time += tpf * speed;      
-        if (animation.getLength() > 0){
-            if (!notified && (time >= animation.getLength() || time < 0)) {
-                if (loopMode == LoopMode.DontLoop) {
-                    // Note that this flag has to be set before calling the notify
-                    // since the notify may start a new animation and then unset
-                    // the flag.
-                    notified = true;
-                }
-                control.notifyAnimCycleDone(this, animation.getName());
-            } 
-        }
-        time = AnimationUtils.clampWrapTime(time, animation.getLength(), loopMode);
-        if (time < 0){
-            // Negative time indicates that speed should be inverted
-            // (for cycle loop mode only)
-            time = -time;
-            speed = -speed;
-        }
-    }
-}
+/*
+ * 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.animation;
+
+import com.jme3.math.FastMath;
+import com.jme3.util.TempVars;
+import java.util.BitSet;
+
+/**
+ * <code>AnimChannel</code> provides controls, such as play, pause,
+ * fast forward, etc, for an animation. The animation
+ * channel may influence the entire model or specific bones of the model's
+ * skeleton. A single model may have multiple animation channels influencing
+ * various parts of its body. For example, a character model may have an
+ * animation channel for its feet, and another for its torso, and
+ * the animations for each channel are controlled independently.
+ * 
+ * @author Kirill Vainer
+ */
+public final class AnimChannel {
+
+    private static final float DEFAULT_BLEND_TIME = 0.15f;
+    
+    private AnimControl control;
+
+    private BitSet affectedBones;
+
+    private Animation animation;
+    private Animation blendFrom;
+    private float time;
+    private float speed;
+    private float timeBlendFrom;
+    private float blendTime;
+    private float speedBlendFrom;
+    private boolean notified=false;
+
+    private LoopMode loopMode, loopModeBlendFrom;
+    
+    private float blendAmount = 1f;
+    private float blendRate   = 0;
+    
+    public AnimChannel(){
+        
+    }
+    
+    public AnimChannel(AnimControl control){
+        this.control = control;
+    }
+
+    /**
+     * Returns the parent control of this AnimChannel.
+     * 
+     * @return the parent control of this AnimChannel.
+     * @see AnimControl
+     */
+    public AnimControl getControl() {
+        return control;
+    }
+    
+    /**
+     * @return The name of the currently playing animation, or null if
+     * none is assigned.
+     *
+     * @see AnimChannel#setAnim(java.lang.String) 
+     */
+    public String getAnimationName() {
+        return animation != null ? animation.getName() : null;
+    }
+
+    /**
+     * @return The loop mode currently set for the animation. The loop mode
+     * determines what will happen to the animation once it finishes
+     * playing.
+     * 
+     * For more information, see the LoopMode enum class.
+     * @see LoopMode
+     * @see AnimChannel#setLoopMode(com.jme3.animation.LoopMode)
+     */
+    public LoopMode getLoopMode() {
+        return loopMode;
+    }
+
+    /**
+     * @param loopMode Set the loop mode for the channel. The loop mode
+     * determines what will happen to the animation once it finishes
+     * playing.
+     *
+     * For more information, see the LoopMode enum class.
+     * @see LoopMode
+     */
+    public void setLoopMode(LoopMode loopMode) {
+        this.loopMode = loopMode;
+    }
+
+    /**
+     * @return The speed that is assigned to the animation channel. The speed
+     * is a scale value starting from 0.0, at 1.0 the animation will play
+     * at its default speed.
+     *
+     * @see AnimChannel#setSpeed(float)
+     */
+    public float getSpeed() {
+        return speed;
+    }
+
+    /**
+     * @param speed Set the speed of the animation channel. The speed
+     * is a scale value starting from 0.0, at 1.0 the animation will play
+     * at its default speed.
+     */
+    public void setSpeed(float speed) {
+        this.speed = speed;
+        if(blendTime>0){
+            this.speedBlendFrom = speed;
+            blendTime = Math.min(blendTime, animation.getLength() / speed);  
+            blendRate = 1/ blendTime;
+        }
+    }
+
+    /**
+     * @return The time of the currently playing animation. The time
+     * starts at 0 and continues on until getAnimMaxTime().
+     *
+     * @see AnimChannel#setTime(float)
+     */
+    public float getTime() {
+        return time;
+    }
+
+    /**
+     * @param time Set the time of the currently playing animation, the time
+     * is clamped from 0 to {@link #getAnimMaxTime()}. 
+     */
+    public void setTime(float time) {
+        this.time = FastMath.clamp(time, 0, getAnimMaxTime());
+    }
+
+    /**
+     * @return The length of the currently playing animation, or zero
+     * if no animation is playing.
+     *
+     * @see AnimChannel#getTime()
+     */
+    public float getAnimMaxTime(){
+        return animation != null ? animation.getLength() : 0f;
+    }
+
+    /**
+     * Set the current animation that is played by this AnimChannel.
+     * <p>
+     * This resets the time to zero, and optionally blends the animation
+     * over <code>blendTime</code> seconds with the currently playing animation.
+     * Notice that this method will reset the control's speed to 1.0.
+     *
+     * @param name The name of the animation to play
+     * @param blendTime The blend time over which to blend the new animation
+     * with the old one. If zero, then no blending will occur and the new
+     * animation will be applied instantly.
+     */
+    public void setAnim(String name, float blendTime){
+        if (name == null)
+            throw new IllegalArgumentException("name cannot be null");
+
+        if (blendTime < 0f)
+            throw new IllegalArgumentException("blendTime cannot be less than zero");
+
+        Animation anim = control.animationMap.get(name);
+        if (anim == null)
+            throw new IllegalArgumentException("Cannot find animation named: '"+name+"'");
+
+        control.notifyAnimChange(this, name);
+
+        if (animation != null && blendTime > 0f){
+            this.blendTime = blendTime;
+            // activate blending
+            blendTime = Math.min(blendTime, anim.getLength() / speed);            
+            blendFrom = animation;
+            timeBlendFrom = time;
+            speedBlendFrom = speed;
+            loopModeBlendFrom = loopMode;
+            blendAmount = 0f;
+            blendRate   = 1f / blendTime;
+        }else{
+            blendFrom = null;
+        }
+
+        animation = anim;
+        time = 0;
+        speed = 1f;
+        loopMode = LoopMode.Loop;
+        notified = false;
+    }
+
+    /**
+     * Set the current animation that is played by this AnimChannel.
+     * <p>
+     * See {@link #setAnim(java.lang.String, float)}.
+     * The blendTime argument by default is 150 milliseconds.
+     * 
+     * @param name The name of the animation to play
+     */
+    public void setAnim(String name){
+        setAnim(name, DEFAULT_BLEND_TIME);
+    }
+
+    /**
+     * Add all the bones of the model's skeleton to be
+     * influenced by this animation channel.
+     */
+    public void addAllBones() {
+        affectedBones = null;
+    }
+
+    /**
+     * Add a single bone to be influenced by this animation channel.
+     */
+    public void addBone(String name) {
+        addBone(control.getSkeleton().getBone(name));
+    }
+
+    /**
+     * Add a single bone to be influenced by this animation channel.
+     */
+    public void addBone(Bone bone) {
+        int boneIndex = control.getSkeleton().getBoneIndex(bone);
+        if(affectedBones == null) {
+            affectedBones = new BitSet(control.getSkeleton().getBoneCount());
+        }
+        affectedBones.set(boneIndex);
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel starting from the
+     * given bone name and going toward the root bone.
+     */
+    public void addToRootBone(String name) {
+        addToRootBone(control.getSkeleton().getBone(name));
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel starting from the
+     * given bone and going toward the root bone.
+     */
+    public void addToRootBone(Bone bone) {
+        addBone(bone);
+        while (bone.getParent() != null) {
+            bone = bone.getParent();
+            addBone(bone);
+        }
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel, starting
+     * from the given named bone and going toward its children.
+     */
+    public void addFromRootBone(String name) {
+        addFromRootBone(control.getSkeleton().getBone(name));
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel, starting
+     * from the given bone and going toward its children.
+     */
+    public void addFromRootBone(Bone bone) {
+        addBone(bone);
+        if (bone.getChildren() == null)
+            return;
+        for (Bone childBone : bone.getChildren()) {
+            addBone(childBone);
+            addFromRootBone(childBone);
+        }
+    }
+
+    BitSet getAffectedBones(){
+        return affectedBones;
+    }
+    
+    public void reset(boolean rewind){
+        if(rewind){
+            setTime(0);        
+            if(control.getSkeleton()!=null){
+                control.getSkeleton().resetAndUpdate();
+            }else{
+                TempVars vars = TempVars.get();
+                update(0, vars);
+                vars.release();    
+            }
+        }
+        animation = null;
+        notified = false;
+    }
+
+    void update(float tpf, TempVars vars) {
+        if (animation == null)
+            return;
+
+        if (blendFrom != null && blendAmount != 1.0f){
+            // The blendFrom anim is set, the actual animation
+            // playing will be set 
+//            blendFrom.setTime(timeBlendFrom, 1f, control, this, vars);
+            blendFrom.setTime(timeBlendFrom, 1f - blendAmount, control, this, vars);
+            
+            timeBlendFrom += tpf * speedBlendFrom;
+            timeBlendFrom = AnimationUtils.clampWrapTime(timeBlendFrom,
+                                          blendFrom.getLength(),
+                                          loopModeBlendFrom);
+            if (timeBlendFrom < 0){
+                timeBlendFrom = -timeBlendFrom;
+                speedBlendFrom = -speedBlendFrom;
+            }
+
+            blendAmount += tpf * blendRate;
+            if (blendAmount > 1f){
+                blendAmount = 1f;
+                blendFrom = null;
+            }
+        }
+        
+        animation.setTime(time, blendAmount, control, this, vars);
+        time += tpf * speed;      
+        if (animation.getLength() > 0){
+            if (!notified && (time >= animation.getLength() || time < 0)) {
+                if (loopMode == LoopMode.DontLoop) {
+                    // Note that this flag has to be set before calling the notify
+                    // since the notify may start a new animation and then unset
+                    // the flag.
+                    notified = true;
+                }
+                control.notifyAnimCycleDone(this, animation.getName());
+            } 
+        }
+        time = AnimationUtils.clampWrapTime(time, animation.getLength(), loopMode);
+        if (time < 0){
+            // Negative time indicates that speed should be inverted
+            // (for cycle loop mode only)
+            time = -time;
+            speed = -speed;
+        }
+    }
+}

+ 19 - 1
jme3-core/src/main/java/com/jme3/animation/Animation.java

@@ -93,6 +93,24 @@ public class Animation implements Savable, Cloneable, JmeCloneable {
         return length;
     }
 
+    /**
+     * Set the length of the animation
+     *
+     * @param length
+     */
+    public void setLength(float length) {
+        this.length = length;
+    }
+
+    /**
+     * Sets the name of the animation
+     *
+     * @param name
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
     /**
      * This method sets the current time of the animation.
      * This method behaves differently for every known track type.
@@ -209,7 +227,7 @@ public class Animation implements Savable, Cloneable, JmeCloneable {
         // isn't cloned at all... even though they all implement clone() methods. -pspeed
         SafeArrayList<Track> newTracks = new SafeArrayList<>(Track.class);
         for( Track track : tracks ) {
-            if( track instanceof ClonableTrack ) {
+            if (track instanceof JmeCloneable) {
                 newTracks.add(cloner.clone(track));
             } else {
                 // this is the part that seems fishy 

+ 4 - 6
jme3-core/src/main/java/com/jme3/animation/AnimationUtils.java

@@ -31,17 +31,15 @@
  */
 package com.jme3.animation;
 
-import static com.jme3.animation.LoopMode.Cycle;
-import static com.jme3.animation.LoopMode.DontLoop;
-import static com.jme3.animation.LoopMode.Loop;
-
 /**
  *
  * @author Nehon
  */
 public class AnimationUtils {
     
-    
+    public AnimationUtils(){
+        
+    }
     /**
      * Clamps the time according to duration and loopMode
      * @param time
@@ -50,7 +48,7 @@ public class AnimationUtils {
      * @return 
      */
      public static float clampWrapTime(float time, float duration, LoopMode loopMode){
-        if (time == 0) {
+         if (time == 0 || duration == 0) {
             return 0; // prevent division by 0 errors
         }        
         switch (loopMode) {

+ 7 - 5
jme3-core/src/main/java/com/jme3/animation/Bone.java

@@ -32,15 +32,15 @@
 package com.jme3.animation;
 
 import com.jme3.export.*;
+import com.jme3.material.MatParamOverride;
 import com.jme3.math.*;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Mesh;
-import com.jme3.scene.Node;
-import com.jme3.scene.Spatial;
+import com.jme3.scene.*;
+import com.jme3.shader.VarType;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
-import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.util.ArrayList;
 
@@ -723,6 +723,8 @@ public final class Bone implements Savable, JmeCloneable {
         if (attachNode == null) {
             attachNode = new Node(name + "_attachnode");
             attachNode.setUserData("AttachedBone", this);
+            //We don't want the node to have a numBone set by a parent node so we force it to null
+            attachNode.addMatParamOverride(new MatParamOverride(VarType.Int, "NumberOfBones", null));
         }
 
         return attachNode;

+ 3 - 1
jme3-core/src/main/java/com/jme3/animation/Skeleton.java

@@ -70,7 +70,7 @@ public final class Skeleton implements Savable, JmeCloneable {
     public Skeleton(Bone[] boneList) {
         this.boneList = boneList;
 
-        List<Bone> rootBoneList = new ArrayList<Bone>();
+        List<Bone> rootBoneList = new ArrayList<>();
         for (int i = boneList.length - 1; i >= 0; i--) {
             Bone b = boneList[i];
             if (b.getParent() == null) {
@@ -289,6 +289,7 @@ public final class Skeleton implements Savable, JmeCloneable {
         return sb.toString();
     }
 
+    @Override
     public void read(JmeImporter im) throws IOException {
         InputCapsule input = im.getCapsule(this);
 
@@ -308,6 +309,7 @@ public final class Skeleton implements Savable, JmeCloneable {
         }
     }
 
+    @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule output = ex.getCapsule(this);
         output.write(rootBones, "rootBones", null);

+ 58 - 86
jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java

@@ -32,28 +32,24 @@
 package com.jme3.animation;
 
 import com.jme3.export.*;
-import com.jme3.material.MatParam;
-import com.jme3.material.Material;
+import com.jme3.material.MatParamOverride;
 import com.jme3.math.FastMath;
 import com.jme3.math.Matrix4f;
-import com.jme3.renderer.RenderManager;
-import com.jme3.renderer.RendererException;
-import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.*;
 import com.jme3.scene.*;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.control.Control;
+import com.jme3.scene.mesh.IndexBuffer;
 import com.jme3.shader.VarType;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.nio.Buffer;
-import java.nio.ByteBuffer;
 import java.nio.FloatBuffer;
-import java.util.HashSet;
-import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -108,10 +104,10 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
      * Bone offset matrices, recreated each frame
      */
     private transient Matrix4f[] offsetMatrices;
-    /**
-     * Material references used for hardware skinning
-     */
-    private Set<Material> materials = new HashSet<Material>();
+
+    
+    private MatParamOverride numberOfBonesParam;
+    private MatParamOverride boneMatricesParam;
     
     /**
      * Serialization only. Do not use.
@@ -120,11 +116,13 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     private void switchToHardware() {
+        numberOfBonesParam.setEnabled(true);
+        boneMatricesParam.setEnabled(true);
+        
         // Next full 10 bones (e.g. 30 on 24 bones)
         int numBones = ((skeleton.getBoneCount() / 10) + 1) * 10;
-        for (Material m : materials) {
-            m.setInt("NumberOfBones", numBones);
-        }
+        numberOfBonesParam.setValue(numBones);
+        
         for (Geometry geometry : targets) {
             Mesh mesh = geometry.getMesh();
             if (mesh != null && mesh.isAnimated()) {
@@ -134,11 +132,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     private void switchToSoftware() {
-        for (Material m : materials) {
-            if (m.getParam("NumberOfBones") != null) {
-                m.clearParam("NumberOfBones");
-            }
-        }
+        numberOfBonesParam.setEnabled(false);
+        boneMatricesParam.setEnabled(false);
+        
         for (Geometry geometry : targets) {
             Mesh mesh = geometry.getMesh();
             if (mesh != null && mesh.isAnimated()) {
@@ -148,17 +144,10 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     private boolean testHardwareSupported(RenderManager rm) {
-        for (Material m : materials) {
-            // Some of the animated mesh(es) do not support hardware skinning,
-            // so it is not supported by the model.
-            if (m.getMaterialDef().getMaterialParam("NumberOfBones") == null) {
-                Logger.getLogger(SkeletonControl.class.getName()).log(Level.WARNING, 
-                        "Not using hardware skinning for {0}, " + 
-                        "because material {1} doesn''t support it.", 
-                        new Object[]{spatial, m.getMaterialDef().getName()});
-                
-                return false;
-            }
+
+        //Only 255 bones max supported with hardware skinning
+        if (skeleton.getBoneCount() > 255) {
+            return false;
         }
 
         switchToHardware();
@@ -177,6 +166,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
      * supported by GPU, it shall be enabled, if its not preferred, or not
      * supported by GPU, then it shall be disabled.
      * 
+     * @param preferred
      * @see #isHardwareSkinningUsed() 
      */
     public void setHardwareSkinningPreferred(boolean preferred) {
@@ -211,6 +201,8 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
             throw new IllegalArgumentException("skeleton cannot be null");
         }
         this.skeleton = skeleton;
+        this.numberOfBonesParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+        this.boneMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
     }
 
     /**
@@ -221,8 +213,8 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         Mesh mesh = geometry.getMesh();
         if (mesh != null && mesh.isAnimated()) {
             targets.add(geometry);
-            materials.add(geometry.getMaterial());
         }
+        
     }
 
     private void findTargets(Node node) {
@@ -237,8 +229,21 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
 
     @Override
     public void setSpatial(Spatial spatial) {
+        Spatial oldSpatial = this.spatial;
         super.setSpatial(spatial);
         updateTargetsAndMaterials(spatial);
+        
+        if (oldSpatial != null) {
+            oldSpatial.removeMatParamOverride(numberOfBonesParam);
+            oldSpatial.removeMatParamOverride(boneMatricesParam);
+        }
+        
+        if (spatial != null) {
+            spatial.removeMatParamOverride(numberOfBonesParam);
+            spatial.removeMatParamOverride(boneMatricesParam);
+            spatial.addMatParamOverride(numberOfBonesParam);
+            spatial.addMatParamOverride(boneMatricesParam);
+        }
     }
 
     private void controlRenderSoftware() {
@@ -257,27 +262,8 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
     
     private void controlRenderHardware() {
         offsetMatrices = skeleton.computeSkinningMatrices();
-        for (Material m : materials) {
-            MatParam currentParam = m.getParam("BoneMatrices");
-
-            if (currentParam != null) {
-                if (currentParam.getValue() != offsetMatrices) {
-                    // Check to see if other SkeletonControl
-                    // is operating on this material, in that case, user
-                    // is sharing materials between models which is NOT allowed
-                    // when hardware skinning used.
-                    
-                    Logger.getLogger(SkeletonControl.class.getName()).log(Level.SEVERE,
-                            "Material instances cannot be shared when hardware skinning is used. " +
-                            "Ensure all models use unique material instances."
-                    );
-                }
-            }
-            
-            m.setParam("BoneMatrices", VarType.Matrix4Array, offsetMatrices);
-        }
+        boneMatricesParam.setValue(offsetMatrices);
     }
-
     
     @Override
     protected void controlRender(RenderManager rm, ViewPort vp) {
@@ -295,7 +281,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
                 if (hwSkinningSupported) {
                     hwSkinningEnabled = true;
                     
-                    Logger.getLogger(SkeletonControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for " + spatial);
+                    Logger.getLogger(SkeletonControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
                 } else {
                     switchToSoftware();
                 }
@@ -419,28 +405,8 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         // were shared then this will share them.
         this.targets = cloner.clone(targets);
         
-        // Not automatic set cloning yet
-        Set<Material> newMaterials = new HashSet<Material>();
-        for( Material m : this.materials ) {
-            Material mClone = cloner.clone(m);
-            newMaterials.add(mClone);
-            if( mClone != m ) {
-                // Material was really cloned so clear the bone matrices in case
-                // this is hardware skinned.  This allows a local version to be
-                // used and will be reset on the material.  Really this just avoids
-                // the 'safety' check in controlRenderHardware().  Right now material
-                // doesn't clone itself with the cloner (and doesn't clone its parameters)
-                // else this would be unnecessary.
-                MatParam boneMatrices = mClone.getParam("BoneMatrices");
-                
-                // ...because for some strange reason you can't clear a non-existant 
-                // parameter.
-                if( boneMatrices != null ) {                    
-                    mClone.clearParam("BoneMatrices");
-                }
-            }
-        }
-        this.materials = newMaterials;
+        this.numberOfBonesParam = cloner.clone(numberOfBonesParam);
+        this.boneMatricesParam = cloner.clone(boneMatricesParam);
     }
          
     /**
@@ -533,7 +499,6 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         if (maxWeightsPerVert <= 0) {
             throw new IllegalStateException("Max weights per vert is incorrectly set!");
         }
-
         int fourMinusMaxWeights = 4 - maxWeightsPerVert;
 
         // NOTE: This code assumes the vertex buffer is in bind pose
@@ -547,14 +512,12 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         fnb.rewind();
 
         // get boneIndexes and weights for mesh
-        ByteBuffer ib = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData();
+        IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
         FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
 
-        ib.rewind();
         wb.rewind();
 
         float[] weights = wb.array();
-        byte[] indices = ib.array();
         int idxWeights = 0;
 
         TempVars vars = TempVars.get();
@@ -592,7 +555,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
 
                 for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
                     float weight = weights[idxWeights];
-                    Matrix4f mat = offsetMatrices[indices[idxWeights++] & 0xff];
+                    Matrix4f mat = offsetMatrices[ib.get(idxWeights++)];
 
                     rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight;
                     ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight;
@@ -665,14 +628,12 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
 
 
         // get boneIndexes and weights for mesh
-        ByteBuffer ib = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData();
+        IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
         FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
 
-        ib.rewind();
         wb.rewind();
 
         float[] weights = wb.array();
-        byte[] indices = ib.array();
         int idxWeights = 0;
 
         TempVars vars = TempVars.get();
@@ -725,7 +686,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
 
                 for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
                     float weight = weights[idxWeights];
-                    Matrix4f mat = offsetMatrices[indices[idxWeights++] & 0xff];
+                    Matrix4f mat = offsetMatrices[ib.get(idxWeights++)];
 
                     rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight;
                     ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight;
@@ -783,7 +744,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(skeleton, "skeleton", null);
-        //Targets and materials don't need to be saved, they'll be gathered on each frame
+        
+        oc.write(numberOfBonesParam, "numberOfBonesParam", null);
+        oc.write(boneMatricesParam, "boneMatricesParam", null);
     }
 
     @Override
@@ -791,6 +754,16 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         super.read(im);
         InputCapsule in = im.getCapsule(this);
         skeleton = (Skeleton) in.readSavable("skeleton", null);
+        
+        numberOfBonesParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null);
+        boneMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null);
+        
+        if (numberOfBonesParam == null) {
+            numberOfBonesParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+            boneMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
+            getSpatial().addMatParamOverride(numberOfBonesParam);
+            getSpatial().addMatParamOverride(boneMatricesParam);
+        }
     }
 
     /**
@@ -800,7 +773,6 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
      */
     private void updateTargetsAndMaterials(Spatial spatial) {
         targets.clear();
-        materials.clear();
 
         if (spatial instanceof Node) {
             findTargets((Node) spatial);

+ 43 - 13
jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java

@@ -39,6 +39,9 @@ import com.jme3.math.Quaternion;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Spatial;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.util.Arrays;
 
@@ -47,7 +50,7 @@ import java.util.Arrays;
  * 
  * @author Marcin Roguski (Kaelthas)
  */
-public class SpatialTrack implements Track {
+public class SpatialTrack implements Track, JmeCloneable {
     
     /** 
      * Translations of the track. 
@@ -63,7 +66,13 @@ public class SpatialTrack implements Track {
      * Scales of the track. 
      */
     private CompactVector3Array scales;
-    
+
+    /**
+     * The spatial to which this track applies.
+     * Note that this is optional, if no spatial is defined, the AnimControl's Spatial will be used.
+     */
+    private Spatial trackSpatial;
+
     /** 
      * The times of the animations frames. 
      */
@@ -97,8 +106,11 @@ public class SpatialTrack implements Track {
      *            the current time of the animation
      */
     public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {
-        Spatial spatial = control.getSpatial();
-        
+        Spatial spatial = trackSpatial;
+        if (spatial == null) {
+            spatial = control.getSpatial();
+        }
+
         Vector3f tempV = vars.vect1;
         Vector3f tempS = vars.vect2;
         Quaternion tempQ = vars.quat1;
@@ -152,11 +164,13 @@ public class SpatialTrack implements Track {
             tempV.interpolateLocal(tempV2, blend);
             tempS.interpolateLocal(tempS2, blend);
         }
-        
-        if (translations != null)
+
+        if (translations != null) {
             spatial.setLocalTranslation(tempV);
-        if (rotations != null)
+        }
+        if (rotations != null) {
             spatial.setLocalRotation(tempQ);
+        }
         if (scales != null) {
             spatial.setLocalScale(tempS);
         }
@@ -235,18 +249,27 @@ public class SpatialTrack implements Track {
     public float getLength() {
             return times == null ? 0 : times[times.length - 1] - times[0];
     }
-    
+
+    @Override
+    public Track clone() {
+        return (Track) jmeClone();
+    }
+
     @Override
     public float[] getKeyFrameTimes() {
         return times;
     }
 
-    /**
-     * This method creates a clone of the current object.
-     * @return a clone of the current object
-     */
+    public void setTrackSpatial(Spatial trackSpatial) {
+        this.trackSpatial = trackSpatial;
+    }
+
+    public Spatial getTrackSpatial() {
+        return trackSpatial;
+    }
+
     @Override
-    public SpatialTrack clone() {
+    public Object jmeClone() {
         int tablesLength = times.length;
 
         float[] timesCopy = this.times.clone();
@@ -257,6 +280,11 @@ public class SpatialTrack implements Track {
         //need to use the constructor here because of the final fields used in this class
         return new SpatialTrack(timesCopy, translationsCopy, rotationsCopy, scalesCopy);
     }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        this.trackSpatial = cloner.clone(((SpatialTrack) original).trackSpatial);
+    }
 	
     @Override
     public void write(JmeExporter ex) throws IOException {
@@ -265,6 +293,7 @@ public class SpatialTrack implements Track {
         oc.write(rotations, "rotations", null);
         oc.write(times, "times", null);
         oc.write(scales, "scales", null);
+        oc.write(trackSpatial, "trackSpatial", null);
     }
 
     @Override
@@ -274,5 +303,6 @@ public class SpatialTrack implements Track {
         rotations = (CompactQuaternionArray) ic.readSavable("rotations", null);
         times = ic.readFloatArray("times", null);
         scales = (CompactVector3Array) ic.readSavable("scales", null);
+        trackSpatial = (Spatial) ic.readSavable("trackSpatial", null);
     }
 }

+ 3 - 2
jme3-core/src/main/java/com/jme3/asset/ImplHandler.java

@@ -32,6 +32,7 @@
 package com.jme3.asset;
 
 import com.jme3.asset.cache.AssetCache;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -279,9 +280,9 @@ final class ImplHandler {
         // Synchronized access must be used for any ops on classToLoaderMap
         // Find the loader ImplThreadLocal for this class
         synchronized (classToLoaderMap){
-            ImplThreadLocal local = classToLoaderMap.get(loaderType);
             // Remove it from the class->loader map
-            classToLoaderMap.remove(loaderType);
+            ImplThreadLocal local = classToLoaderMap.remove(loaderType);
+            if (local == null) return;
             // Remove it from the extension->loader map
             for (String extension : local.getExtensions()){
                 extensionToLoaderMap.remove(extension);

+ 715 - 720
jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java

@@ -1,720 +1,715 @@
-/*
- * 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.cinematic;
-
-import com.jme3.animation.LoopMode;
-import com.jme3.app.Application;
-import com.jme3.app.state.AppState;
-import com.jme3.app.state.AppStateManager;
-import com.jme3.cinematic.events.AbstractCinematicEvent;
-import com.jme3.cinematic.events.CinematicEvent;
-import com.jme3.export.*;
-import com.jme3.renderer.Camera;
-import com.jme3.renderer.RenderManager;
-import com.jme3.scene.CameraNode;
-import com.jme3.scene.Node;
-import com.jme3.scene.control.CameraControl;
-import com.jme3.scene.control.CameraControl.ControlDirection;
-import java.io.IOException;
-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;
-
-/**
- * An appstate for composing and playing cut scenes in a game. The cinematic
- * schedules CinematicEvents over a timeline. Once the Cinematic created it has
- * to be attached to the stateManager.
- *
- * You can add various CinematicEvents to a Cinematic, see package
- * com.jme3.cinematic.events
- *
- * Two main methods can be used to add an event :
- *
- * @see Cinematic#addCinematicEvent(float,
- * com.jme3.cinematic.events.CinematicEvent) , that adds an event at the given
- * time form the cinematic start.
- *
- * @see
- * Cinematic#enqueueCinematicEvent(com.jme3.cinematic.events.CinematicEvent)
- * that enqueue events one after the other according to their initialDuration
- *
- * a cinematic has convenient methods to handle the playback :
- * @see Cinematic#play()
- * @see Cinematic#pause()
- * @see Cinematic#stop()
- *
- * A cinematic is itself a CinematicEvent, meaning you can embed several
- * Cinematics Embed cinematics must not be added to the stateManager though.
- *
- * Cinematic has a way to handle several point of view by creating CameraNode
- * over a cam and activating them on schedule.
- * @see Cinematic#bindCamera(java.lang.String, com.jme3.renderer.Camera)
- * @see Cinematic#activateCamera(float, java.lang.String)
- * @see Cinematic#setActiveCamera(java.lang.String)
- *
- * @author Nehon
- */
-public class Cinematic extends AbstractCinematicEvent implements AppState {
-
-    private static final Logger logger = Logger.getLogger(Application.class.getName());
-    private Node scene;
-    protected TimeLine timeLine = new TimeLine();
-    private int lastFetchedKeyFrame = -1;
-    private List<CinematicEvent> cinematicEvents = new ArrayList<CinematicEvent>();
-    private Map<String, CameraNode> cameras = new HashMap<String, CameraNode>();
-    private CameraNode currentCam;
-    private boolean initialized = false;
-    private Map<String, Map<Object, Object>> eventsData;
-    private float nextEnqueue = 0;
-
-    /**
-     * Used for serialization creates a cinematic, don't use this constructor
-     * directly
-     */
-    public Cinematic() {
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     */
-    public Cinematic(Node scene) {
-        this.scene = scene;
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     * @param initialDuration the duration of the cinematic (without considering
-     * the speed)
-     */
-    public Cinematic(Node scene, float initialDuration) {
-        super(initialDuration);
-        this.scene = scene;
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     * @param loopMode tells if this cinematic should be looped or not
-     */
-    public Cinematic(Node scene, LoopMode loopMode) {
-        super(loopMode);
-        this.scene = scene;
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     * @param initialDuration the duration of the cinematic (without considering
-     * the speed)
-     * @param loopMode tells if this cinematic should be looped or not
-     */
-    public Cinematic(Node scene, float initialDuration, LoopMode loopMode) {
-        super(initialDuration, loopMode);
-        this.scene = scene;
-    }
-
-    /**
-     * called internally
-     */
-    @Override
-    public void onPlay() {
-        if (isInitialized()) {
-            if (playState == PlayState.Paused) {
-                for (int i = 0; i < cinematicEvents.size(); i++) {
-                    CinematicEvent ce = cinematicEvents.get(i);
-                    if (ce.getPlayState() == PlayState.Paused) {
-                        ce.play();
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * called internally
-     */
-    @Override
-    public void onStop() {
-        time = 0;
-        lastFetchedKeyFrame = -1;
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            ce.setTime(0);
-            ce.forceStop();
-        }
-        setEnableCurrentCam(false);
-    }
-
-    /**
-     * called internally
-     */
-    @Override
-    public void onPause() {
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            if (ce.getPlayState() == PlayState.Playing) {
-                ce.pause();
-            }
-        }
-    }
-
-    /**
-     * used internally for serialization
-     *
-     * @param ex
-     * @throws IOException
-     */
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-
-        oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null);
-        oc.writeStringSavableMap(cameras, "cameras", null);
-        oc.write(timeLine, "timeLine", null);
-
-    }
-
-    /**
-     * used internally for serialization
-     *
-     * @param im
-     * @throws IOException
-     */
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-
-        cinematicEvents = ic.readSavableArrayList("cinematicEvents", null);
-        cameras = (Map<String, CameraNode>) ic.readStringSavableMap("cameras", null);
-        timeLine = (TimeLine) ic.readSavable("timeLine", null);
-    }
-
-    /**
-     * sets the speed of the cinematic. Note that it will set the speed of all
-     * events in the cinematic. 1 is normal speed. use 0.5f to make the
-     * cinematic twice slower, use 2 to make it twice faster
-     *
-     * @param speed the speed
-     */
-    @Override
-    public void setSpeed(float speed) {
-        super.setSpeed(speed);
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            ce.setSpeed(speed);
-        }
-
-
-    }
-
-    /**
-     * used internally
-     *
-     * @param stateManager the state manager
-     * @param app the application
-     */
-    public void initialize(AppStateManager stateManager, Application app) {
-        initEvent(app, this);
-        for (CinematicEvent cinematicEvent : cinematicEvents) {
-            cinematicEvent.initEvent(app, this);
-        }
-
-        initialized = true;
-    }
-
-    /**
-     * used internally
-     *
-     * @return
-     */
-    public boolean isInitialized() {
-        return initialized;
-    }
-
-    /**
-     * passing true has the same effect as play() you should use play(),
-     * pause(), stop() to handle the cinematic playing state.
-     *
-     * @param enabled true or false
-     */
-    public void setEnabled(boolean enabled) {
-        if (enabled) {
-            play();
-        }
-    }
-
-    /**
-     * return true if the cinematic appstate is enabled (the cinematic is
-     * playing)
-     *
-     * @return true if enabled
-     */
-    public boolean isEnabled() {
-        return playState == PlayState.Playing;
-    }
-
-    /**
-     * called internally
-     *
-     * @param stateManager the state manager
-     */
-    public void stateAttached(AppStateManager stateManager) {
-    }
-
-    /**
-     * called internally
-     *
-     * @param stateManager the state manager
-     */
-    public void stateDetached(AppStateManager stateManager) {
-        stop();
-    }
-
-    /**
-     * called internally don't call it directly.
-     *
-     * @param tpf
-     */
-    public void update(float tpf) {
-        if (isInitialized()) {
-            internalUpdate(tpf);
-        }
-    }
-
-    /**
-     * used internally, don't call this directly.
-     *
-     * @param tpf
-     */
-    @Override
-    public void onUpdate(float tpf) {
-        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
-
-        //iterate to make sure every key frame is triggered
-        for (int i = lastFetchedKeyFrame + 1; i <= keyFrameIndex; i++) {
-            KeyFrame keyFrame = timeLine.get(i);
-            if (keyFrame != null) {
-                keyFrame.trigger();
-            }
-        }
-
-        
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            ce.internalUpdate(tpf);
-        }
-
-        
-        lastFetchedKeyFrame = keyFrameIndex;
-    }
-
-    /**
-     * This is used internally but can be called to shuffle through the
-     * cinematic.
-     *
-     * @param time the time to shuffle to.
-     */
-    @Override
-    public void setTime(float time) {
-
-        //stopping all events
-        onStop();
-        super.setTime(time);
-
-        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
-        //triggering all the event from start to "time" 
-        //then computing timeOffset for each event
-        for (int i = 0; i <= keyFrameIndex; i++) {
-            KeyFrame keyFrame = timeLine.get(i);
-            if (keyFrame != null) {
-                for (CinematicEvent ce : keyFrame.getCinematicEvents()) {
-                    float t = this.time - timeLine.getKeyFrameTime(keyFrame);
-                    if (t >= 0 && (t <= ce.getInitialDuration() || ce.getLoopMode() != LoopMode.DontLoop)) {
-                        ce.play();
-                    }
-                    ce.setTime(t);
-                }
-            }
-        }
-        lastFetchedKeyFrame = keyFrameIndex;
-        if (playState != PlayState.Playing) {
-            pause();
-        }
-    }
-
-    /**
-     * Adds a cinematic event to this cinematic at the given timestamp. This
-     * operation returns a keyFrame
-     *
-     * @param timeStamp the time when the event will start after the beginning of
-     * the cinematic
-     * @param cinematicEvent the cinematic event
-     * @return the keyFrame for that event.
-     */
-    public KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
-        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
-        if (keyFrame == null) {
-            keyFrame = new KeyFrame();
-            timeLine.addKeyFrameAtTime(timeStamp, keyFrame);
-        }
-        keyFrame.cinematicEvents.add(cinematicEvent);
-        cinematicEvents.add(cinematicEvent);
-        if (isInitialized()) {
-            cinematicEvent.initEvent(null, this);
-        }
-        return keyFrame;
-    }
-
-    /**
-     * enqueue a cinematic event to a cinematic. This is a handy method when you
-     * want to chain event of a given duration without knowing their initial
-     * duration
-     *
-     * @param cinematicEvent the cinematic event to enqueue
-     * @return the timestamp the event was scheduled.
-     */
-    public float enqueueCinematicEvent(CinematicEvent cinematicEvent) {
-        float scheduleTime = nextEnqueue;
-        addCinematicEvent(scheduleTime, cinematicEvent);
-        nextEnqueue += cinematicEvent.getInitialDuration();
-        return scheduleTime;
-    }
-
-    /**
-     * removes the first occurrence found of the given cinematicEvent.
-     *
-     * @param cinematicEvent the cinematicEvent to remove
-     * @return true if the element has been removed
-     */
-    public boolean removeCinematicEvent(CinematicEvent cinematicEvent) {
-        cinematicEvent.dispose();
-        cinematicEvents.remove(cinematicEvent);
-        for (KeyFrame keyFrame : timeLine.values()) {
-            if (keyFrame.cinematicEvents.remove(cinematicEvent)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * removes the first occurrence found of the given cinematicEvent for the
-     * given time stamp.
-     *
-     * @param timeStamp the timestamp when the cinematicEvent has been added
-     * @param cinematicEvent the cinematicEvent to remove
-     * @return true if the element has been removed
-     */
-    public boolean removeCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
-        cinematicEvent.dispose();
-        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
-        return removeCinematicEvent(keyFrame, cinematicEvent);
-    }
-
-    /**
-     * removes the first occurrence found of the given cinematicEvent for the
-     * given keyFrame
-     *
-     * @param keyFrame the keyFrame returned by the addCinematicEvent method.
-     * @param cinematicEvent the cinematicEvent to remove
-     * @return true if the element has been removed
-     */
-    public boolean removeCinematicEvent(KeyFrame keyFrame, CinematicEvent cinematicEvent) {
-        cinematicEvent.dispose();
-        boolean ret = keyFrame.cinematicEvents.remove(cinematicEvent);
-        cinematicEvents.remove(cinematicEvent);
-        if (keyFrame.isEmpty()) {
-            timeLine.removeKeyFrame(keyFrame.getIndex());
-        }
-        return ret;
-    }
-
-    /**
-     * called internally
-     *
-     * @see AppState#render(com.jme3.renderer.RenderManager) 
-     */
-    public void render(RenderManager rm) {
-    }
-
-    /**
-     * called internally
-     *
-     * @see AppState#postRender()
-     */
-    public void postRender() {
-    }
-
-    /**
-     * called internally
-     *
-     * @see AppState#cleanup()
-     */
-    public void cleanup() {
-    }
-
-    /**
-     * fits the duration of the cinematic to the duration of all its child
-     * cinematic events
-     */
-    public void fitDuration() {
-        KeyFrame kf = timeLine.getKeyFrameAtIndex(timeLine.getLastKeyFrameIndex());
-        float d = 0;
-        for (int i = 0; i < kf.getCinematicEvents().size(); i++) {
-            CinematicEvent ce = kf.getCinematicEvents().get(i);
-            float dur = timeLine.getKeyFrameTime(kf) + ce.getDuration() * ce.getSpeed();
-            if (d < dur) {
-                d = dur;
-            }
-        }
-
-        initialDuration = d;
-    }
-
-    /**
-     * Binds a camera to this cinematic, tagged by a unique name. This methods
-     * creates and returns a CameraNode for the cam and attach it to the scene.
-     * The control direction is set to SpatialToCamera. This camera Node can
-     * then be used in other events to handle the camera movements during the
-     * playback
-     *
-     * @param cameraName the unique tag the camera should have
-     * @param cam the scene camera.
-     * @return the created CameraNode.
-     */
-    public CameraNode bindCamera(String cameraName, Camera cam) {
-        if (cameras.containsKey(cameraName)) {
-            throw new IllegalArgumentException("Camera " + cameraName + " is already binded to this cinematic");
-        }
-        CameraNode node = new CameraNode(cameraName, cam);
-        node.setControlDir(ControlDirection.SpatialToCamera);
-        node.getControl(CameraControl.class).setEnabled(false);
-        cameras.put(cameraName, node);
-        scene.attachChild(node);
-        return node;
-    }
-
-    /**
-     * returns a cameraNode given its name
-     *
-     * @param cameraName the camera name (as registered in
-     * Cinematic#bindCamera())
-     * @return the cameraNode for this name
-     */
-    public CameraNode getCamera(String cameraName) {
-        return cameras.get(cameraName);
-    }
-
-    /**
-     * enable/disable the camera control of the cameraNode of the current cam
-     *
-     * @param enabled
-     */
-    private void setEnableCurrentCam(boolean enabled) {
-        if (currentCam != null) {
-            currentCam.getControl(CameraControl.class).setEnabled(enabled);
-        }
-    }
-
-    /**
-     * Sets the active camera instantly (use activateCamera if you want to
-     * schedule that event)
-     *
-     * @param cameraName the camera name (as registered in
-     * Cinematic#bindCamera())
-     */
-    public void setActiveCamera(String cameraName) {
-        setEnableCurrentCam(false);
-        currentCam = cameras.get(cameraName);
-        if (currentCam == null) {
-            logger.log(Level.WARNING, "{0} is not a camera bond to the cinematic, cannot activate", cameraName);
-        }
-        setEnableCurrentCam(true);
-    }
-
-    /**
-     * schedule an event that will activate the camera at the given time
-     *
-     * @param timeStamp the time to activate the cam
-     * @param cameraName the camera name (as registered in
-     * Cinematic#bindCamera())
-     */
-    public void activateCamera(final float timeStamp, final String cameraName) {
-        addCinematicEvent(timeStamp, new AbstractCinematicEvent() {
-            @Override
-            public void play() {
-                super.play();
-                stop();
-            }
-
-            @Override
-            public void onPlay() {
-                setActiveCamera(cameraName);
-            }
-
-            @Override
-            public void onUpdate(float tpf) {
-            }
-
-            @Override
-            public void onStop() {
-            }
-
-            @Override
-            public void onPause() {
-            }
-
-            @Override
-            public void forceStop() {
-            }
-
-            @Override
-            public void setTime(float time) {
-                play();
-            }
-        });
-    }
-
-    /**
-     * returns the complete eventdata map
-     *
-     * @return the eventdata map
-     */
-    private Map<String, Map<Object, Object>> getEventsData() {
-        if (eventsData == null) {
-            eventsData = new HashMap<String, Map<Object, Object>>();
-        }
-        return eventsData;
-    }
-
-    /**
-     * used internally put an eventdata in the cinematic
-     *
-     * @param type the type of data
-     * @param key the key
-     * @param object the data
-     */
-    public void putEventData(String type, Object key, Object object) {
-        Map<String, Map<Object, Object>> data = getEventsData();
-        Map<Object, Object> row = data.get(type);
-        if (row == null) {
-            row = new HashMap<Object, Object>();
-        }
-        row.put(key, object);
-        data.put(type, row);
-    }
-
-    /**
-     * used internally return and event data
-     *
-     * @param type the type of data
-     * @param key the key
-     * @return
-     */
-    public Object getEventData(String type, Object key) {
-        if (eventsData != null) {
-            Map<Object, Object> row = eventsData.get(type);
-            if (row != null) {
-                return row.get(key);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Used internally remove an eventData
-     *
-     * @param type the type of data
-     * @param key the key of the data
-     */
-    public void removeEventData(String type, Object key) {
-        if (eventsData != null) {
-            Map<Object, Object> row = eventsData.get(type);
-            if (row != null) {
-                row.remove(key);
-            }
-        }
-    }
-
-    /**
-     * sets the scene to use for this cinematic it is expected that the scene is
-     * added before adding events to the cinematic
-     *
-     * @param scene the scene where the cinematic should take place.
-     */
-    public void setScene(Node scene) {
-        this.scene = scene;
-    }
-
-    /**
-     * return the scene where the cinematic occur
-     *
-     * @return the scene
-     */
-    public Node getScene() {
-        return scene;
-    }
-
-    /**
-     * clear the cinematic of its events.
-     */
-    public void clear() {
-        dispose();
-        cinematicEvents.clear();
-        timeLine.clear();
-        if (eventsData != null) {
-            eventsData.clear();
-        }
-    }
-
-    /**
-     * used internally to cleanup the cinematic. Called when the clear() method
-     * is called
-     */
-    @Override
-    public void dispose() {
-        for (CinematicEvent event : cinematicEvents) {
-            event.dispose();
-        }
-    }
-}
+/*
+ * 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.cinematic;
+
+import com.jme3.animation.LoopMode;
+import com.jme3.app.Application;
+import com.jme3.app.state.AppState;
+import com.jme3.app.state.AppStateManager;
+import com.jme3.cinematic.events.AbstractCinematicEvent;
+import com.jme3.cinematic.events.CameraEvent;
+import com.jme3.cinematic.events.CinematicEvent;
+import com.jme3.export.*;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.CameraNode;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.CameraControl;
+import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.scene.control.Control;
+import java.io.IOException;
+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;
+
+/**
+ * An appstate for composing and playing cut scenes in a game. The cinematic
+ * schedules CinematicEvents over a timeline. Once the Cinematic created it has
+ * to be attached to the stateManager.
+ *
+ * You can add various CinematicEvents to a Cinematic, see package
+ * com.jme3.cinematic.events
+ *
+ * Two main methods can be used to add an event :
+ *
+ * @see Cinematic#addCinematicEvent(float,
+ * com.jme3.cinematic.events.CinematicEvent) , that adds an event at the given
+ * time form the cinematic start.
+ *
+ * @see
+ * Cinematic#enqueueCinematicEvent(com.jme3.cinematic.events.CinematicEvent)
+ * that enqueue events one after the other according to their initialDuration
+ *
+ * a cinematic has convenient methods to handle the playback :
+ * @see Cinematic#play()
+ * @see Cinematic#pause()
+ * @see Cinematic#stop()
+ *
+ * A cinematic is itself a CinematicEvent, meaning you can embed several
+ * Cinematics Embed cinematics must not be added to the stateManager though.
+ *
+ * Cinematic has a way to handle several point of view by creating CameraNode
+ * over a cam and activating them on schedule.
+ * @see Cinematic#bindCamera(java.lang.String, com.jme3.renderer.Camera)
+ * @see Cinematic#activateCamera(float, java.lang.String)
+ * @see Cinematic#setActiveCamera(java.lang.String)
+ *
+ * @author Nehon
+ */
+public class Cinematic extends AbstractCinematicEvent implements AppState {
+
+    private static final Logger logger = Logger.getLogger(Cinematic.class.getName());
+    private Node scene;
+    protected TimeLine timeLine = new TimeLine();
+    private int lastFetchedKeyFrame = -1;
+    private List<CinematicEvent> cinematicEvents = new ArrayList<>();
+    private Map<String, CameraNode> cameras = new HashMap<>();
+    private CameraNode currentCam;
+    private boolean initialized = false;
+    private Map<String, Map<Object, Object>> eventsData;
+    private float nextEnqueue = 0;
+
+    /**
+     * Used for serialization creates a cinematic, don't use this constructor
+     * directly
+     */
+    public Cinematic() {
+        super();
+    }
+
+    public Cinematic(float initialDuration) {
+        super(initialDuration);
+    }
+
+    public Cinematic(LoopMode loopMode) {
+        super(loopMode);
+    }
+
+    public Cinematic(float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     */
+    public Cinematic(Node scene) {
+        this.scene = scene;
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     * @param initialDuration the duration of the cinematic (without considering
+     * the speed)
+     */
+    public Cinematic(Node scene, float initialDuration) {
+        super(initialDuration);
+        this.scene = scene;
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     * @param loopMode tells if this cinematic should be looped or not
+     */
+    public Cinematic(Node scene, LoopMode loopMode) {
+        super(loopMode);
+        this.scene = scene;
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     * @param initialDuration the duration of the cinematic (without considering
+     * the speed)
+     * @param loopMode tells if this cinematic should be looped or not
+     */
+    public Cinematic(Node scene, float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+        this.scene = scene;
+    }
+
+    /**
+     * called internally
+     */
+    @Override
+    public void onPlay() {
+        if (isInitialized()) {
+            if (playState == PlayState.Paused) {
+                for (int i = 0; i < cinematicEvents.size(); i++) {
+                    CinematicEvent ce = cinematicEvents.get(i);
+                    if (ce.getPlayState() == PlayState.Paused) {
+                        ce.play();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * called internally
+     */
+    @Override
+    public void onStop() {
+        time = 0;
+        lastFetchedKeyFrame = -1;
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            ce.setTime(0);
+            ce.forceStop();
+        }
+        setEnableCurrentCam(false);
+    }
+
+    /**
+     * called internally
+     */
+    @Override
+    public void onPause() {
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            if (ce.getPlayState() == PlayState.Playing) {
+                ce.pause();
+            }
+        }
+    }
+
+    /**
+     * used internally for serialization
+     *
+     * @param ex
+     * @throws IOException
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(cinematicEvents.toArray(new CinematicEvent[cinematicEvents.size()]), "cinematicEvents", null);
+        oc.writeStringSavableMap(cameras, "cameras", null);
+        oc.write(timeLine, "timeLine", null);
+
+    }
+
+    /**
+     * used internally for serialization
+     *
+     * @param im
+     * @throws IOException
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        Savable[] events = ic.readSavableArray("cinematicEvents", null);
+        for (Savable c : events) {
+//            addCinematicEvent(((CinematicEvent) c).getTime(), (CinematicEvent) c)
+            cinematicEvents.add((CinematicEvent) c);
+        }
+        cameras = (Map<String, CameraNode>) ic.readStringSavableMap("cameras", null);
+        timeLine = (TimeLine) ic.readSavable("timeLine", null);
+    }
+
+    /**
+     * sets the speed of the cinematic. Note that it will set the speed of all
+     * events in the cinematic. 1 is normal speed. use 0.5f to make the
+     * cinematic twice slower, use 2 to make it twice faster
+     *
+     * @param speed the speed
+     */
+    @Override
+    public void setSpeed(float speed) {
+        super.setSpeed(speed);
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            ce.setSpeed(speed);
+        }
+
+    }
+
+    /**
+     * used internally
+     *
+     * @param stateManager the state manager
+     * @param app the application
+     */
+    public void initialize(AppStateManager stateManager, Application app) {
+        initEvent(app, this);
+        for (CinematicEvent cinematicEvent : cinematicEvents) {
+            cinematicEvent.initEvent(app, this);
+        }
+        if(!cameras.isEmpty()){
+            for(CameraNode n : cameras.values()){
+                n.setCamera(app.getCamera());
+            }
+        }
+        initialized = true;
+    }
+
+    /**
+     * used internally
+     *
+     * @return
+     */
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    /**
+     * passing true has the same effect as play() you should use play(),
+     * pause(), stop() to handle the cinematic playing state.
+     *
+     * @param enabled true or false
+     */
+    public void setEnabled(boolean enabled) {
+        if (enabled) {
+            play();
+        }
+    }
+
+    /**
+     * return true if the cinematic appstate is enabled (the cinematic is
+     * playing)
+     *
+     * @return true if enabled
+     */
+    public boolean isEnabled() {
+        return playState == PlayState.Playing;
+    }
+
+    /**
+     * called internally
+     *
+     * @param stateManager the state manager
+     */
+    public void stateAttached(AppStateManager stateManager) {
+    }
+
+    /**
+     * called internally
+     *
+     * @param stateManager the state manager
+     */
+    public void stateDetached(AppStateManager stateManager) {
+        stop();
+    }
+
+    /**
+     * called internally don't call it directly.
+     *
+     * @param tpf
+     */
+    @Override
+    public void update(float tpf) {
+        if (isInitialized() && playState == PlayState.Playing) {
+            internalUpdate(tpf);
+        }
+    }
+
+    /**
+     * used internally, don't call this directly.
+     *
+     * @param tpf
+     */
+    @Override
+    public void onUpdate(float tpf) {
+        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
+
+        //iterate to make sure every key frame is triggered
+        for (int i = lastFetchedKeyFrame + 1; i <= keyFrameIndex; i++) {
+            KeyFrame keyFrame = timeLine.get(i);
+            if (keyFrame != null) {
+                keyFrame.trigger();
+            }
+        }
+
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            ce.internalUpdate(tpf);
+        }
+
+        lastFetchedKeyFrame = keyFrameIndex;
+    }
+
+    /**
+     * This is used internally but can be called to shuffle through the
+     * cinematic.
+     *
+     * @param time the time to shuffle to.
+     */
+    @Override
+    public void setTime(float time) {
+
+        //stopping all events
+        onStop();
+        super.setTime(time);
+
+        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
+        //triggering all the event from start to "time" 
+        //then computing timeOffset for each event
+        for (int i = 0; i <= keyFrameIndex; i++) {
+            KeyFrame keyFrame = timeLine.get(i);
+            if (keyFrame != null) {
+                for (CinematicEvent ce : keyFrame.getCinematicEvents()) {
+                    float t = this.time - timeLine.getKeyFrameTime(keyFrame);
+                    if (t >= 0 && (t <= ce.getInitialDuration() || ce.getLoopMode() != LoopMode.DontLoop)) {
+                        ce.play();
+                    }
+                    ce.setTime(t);
+                }
+            }
+        }
+        lastFetchedKeyFrame = keyFrameIndex;
+        if (playState != PlayState.Playing) {
+            pause();
+        }
+    }
+
+    /**
+     * Adds a cinematic event to this cinematic at the given timestamp. This
+     * operation returns a keyFrame
+     *
+     * @param timeStamp the time when the event will start after the beginning
+     * of the cinematic
+     * @param cinematicEvent the cinematic event
+     * @return the keyFrame for that event.
+     */
+    public KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
+        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
+        if (keyFrame == null) {
+            keyFrame = new KeyFrame();
+            timeLine.addKeyFrameAtTime(timeStamp, keyFrame);
+        }
+        keyFrame.cinematicEvents.add(cinematicEvent);
+        cinematicEvents.add(cinematicEvent);
+        if (isInitialized()) {
+            cinematicEvent.initEvent(null, this);
+        }
+        return keyFrame;
+    }
+
+    /**
+     * enqueue a cinematic event to a cinematic. This is a handy method when you
+     * want to chain event of a given duration without knowing their initial
+     * duration
+     *
+     * @param cinematicEvent the cinematic event to enqueue
+     * @return the timestamp the event was scheduled.
+     */
+    public float enqueueCinematicEvent(CinematicEvent cinematicEvent) {
+        float scheduleTime = nextEnqueue;
+        addCinematicEvent(scheduleTime, cinematicEvent);
+        nextEnqueue += cinematicEvent.getInitialDuration();
+        return scheduleTime;
+    }
+
+    /**
+     * removes the first occurrence found of the given cinematicEvent.
+     *
+     * @param cinematicEvent the cinematicEvent to remove
+     * @return true if the element has been removed
+     */
+    public boolean removeCinematicEvent(CinematicEvent cinematicEvent) {
+        cinematicEvent.dispose();
+        cinematicEvents.remove(cinematicEvent);
+        for (KeyFrame keyFrame : timeLine.values()) {
+            if (keyFrame.cinematicEvents.remove(cinematicEvent)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * removes the first occurrence found of the given cinematicEvent for the
+     * given time stamp.
+     *
+     * @param timeStamp the timestamp when the cinematicEvent has been added
+     * @param cinematicEvent the cinematicEvent to remove
+     * @return true if the element has been removed
+     */
+    public boolean removeCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
+        cinematicEvent.dispose();
+        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
+        return removeCinematicEvent(keyFrame, cinematicEvent);
+    }
+
+    /**
+     * removes the first occurrence found of the given cinematicEvent for the
+     * given keyFrame
+     *
+     * @param keyFrame the keyFrame returned by the addCinematicEvent method.
+     * @param cinematicEvent the cinematicEvent to remove
+     * @return true if the element has been removed
+     */
+    public boolean removeCinematicEvent(KeyFrame keyFrame, CinematicEvent cinematicEvent) {
+        cinematicEvent.dispose();
+        boolean ret = keyFrame.cinematicEvents.remove(cinematicEvent);
+        cinematicEvents.remove(cinematicEvent);
+        if (keyFrame.isEmpty()) {
+            timeLine.removeKeyFrame(keyFrame.getIndex());
+        }
+        return ret;
+    }
+
+    /**
+     * called internally
+     *
+     * @see AppState#render(com.jme3.renderer.RenderManager)
+     */
+    public void render(RenderManager rm) {
+    }
+
+    /**
+     * called internally
+     *
+     * @see AppState#postRender()
+     */
+    public void postRender() {
+    }
+
+    /**
+     * called internally
+     *
+     * @see AppState#cleanup()
+     */
+    public void cleanup() {
+    }
+
+    /**
+     * fits the duration of the cinematic to the duration of all its child
+     * cinematic events
+     */
+    public void fitDuration() {
+        KeyFrame kf = timeLine.getKeyFrameAtIndex(timeLine.getLastKeyFrameIndex());
+        float d = 0;
+        for (int i = 0; i < kf.getCinematicEvents().size(); i++) {
+            CinematicEvent ce = kf.getCinematicEvents().get(i);
+            float dur = timeLine.getKeyFrameTime(kf) + ce.getDuration() * ce.getSpeed();
+            if (d < dur) {
+                d = dur;
+            }
+        }
+
+        initialDuration = d;
+    }
+
+    /**
+     * Binds a camera to this cinematic, tagged by a unique name. This methods
+     * creates and returns a CameraNode for the cam and attach it to the scene.
+     * The control direction is set to SpatialToCamera. This camera Node can
+     * then be used in other events to handle the camera movements during the
+     * playback
+     *
+     * @param cameraName the unique tag the camera should have
+     * @param cam the scene camera.
+     * @return the created CameraNode.
+     */
+    public CameraNode bindCamera(String cameraName, Camera cam) {
+        if (cameras.containsKey(cameraName)) {
+            throw new IllegalArgumentException("Camera " + cameraName + " is already binded to this cinematic");
+        }
+        CameraNode node = new CameraNode(cameraName, cam);
+        node.setControlDir(ControlDirection.SpatialToCamera);
+        node.getControl(CameraControl.class).setEnabled(false);
+        cameras.put(cameraName, node);
+        scene.attachChild(node);
+        return node;
+    }
+
+    /**
+     * returns a cameraNode given its name
+     *
+     * @param cameraName the camera name (as registered in
+     * Cinematic#bindCamera())
+     * @return the cameraNode for this name
+     */
+    public CameraNode getCamera(String cameraName) {
+        return cameras.get(cameraName);
+    }
+
+    /**
+     * enable/disable the camera control of the cameraNode of the current cam
+     *
+     * @param enabled
+     */
+    private void setEnableCurrentCam(boolean enabled) {
+        if (currentCam != null) {
+            currentCam.getControl(CameraControl.class).setEnabled(enabled);
+        }
+    }
+
+    /**
+     * Sets the active camera instantly (use activateCamera if you want to
+     * schedule that event)
+     *
+     * @param cameraName the camera name (as registered in
+     * Cinematic#bindCamera())
+     */
+    public void setActiveCamera(String cameraName) {
+        setEnableCurrentCam(false);
+        currentCam = cameras.get(cameraName);
+        if (currentCam == null) {
+            logger.log(Level.WARNING, "{0} is not a camera bond to the cinematic, cannot activate", cameraName);
+        }
+        setEnableCurrentCam(true);
+    }
+
+    /**
+     * schedule an event that will activate the camera at the given time
+     *
+     * @param timeStamp the time to activate the cam
+     * @param cameraName the camera name (as registered in
+     * Cinematic#bindCamera())
+     */
+    public void activateCamera(final float timeStamp, final String cameraName) {
+        addCinematicEvent(timeStamp, new CameraEvent(this, cameraName));
+    }
+
+    /**
+     * returns the complete eventdata map
+     *
+     * @return the eventdata map
+     */
+    private Map<String, Map<Object, Object>> getEventsData() {
+        if (eventsData == null) {
+            eventsData = new HashMap<String, Map<Object, Object>>();
+        }
+        return eventsData;
+    }
+
+    /**
+     * used internally put an eventdata in the cinematic
+     *
+     * @param type the type of data
+     * @param key the key
+     * @param object the data
+     */
+    public void putEventData(String type, Object key, Object object) {
+        Map<String, Map<Object, Object>> data = getEventsData();
+        Map<Object, Object> row = data.get(type);
+        if (row == null) {
+            row = new HashMap<Object, Object>();
+        }
+        row.put(key, object);
+        data.put(type, row);
+    }
+
+    /**
+     * used internally return and event data
+     *
+     * @param type the type of data
+     * @param key the key
+     * @return
+     */
+    public Object getEventData(String type, Object key) {
+        if (eventsData != null) {
+            Map<Object, Object> row = eventsData.get(type);
+            if (row != null) {
+                return row.get(key);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Used internally remove an eventData
+     *
+     * @param type the type of data
+     * @param key the key of the data
+     */
+    public void removeEventData(String type, Object key) {
+        if (eventsData != null) {
+            Map<Object, Object> row = eventsData.get(type);
+            if (row != null) {
+                row.remove(key);
+            }
+        }
+    }
+
+    /**
+     * sets the scene to use for this cinematic it is expected that the scene is
+     * added before adding events to the cinematic
+     *
+     * @param scene the scene where the cinematic should take place.
+     */
+    public void setScene(Node scene) {
+        this.scene = scene;
+        if(!cameras.isEmpty()){
+            for(CameraNode n : cameras.values()){
+                this.scene.attachChild(n);
+            }
+        }
+    }
+
+    /**
+     * return the scene where the cinematic occur
+     *
+     * @return the scene
+     */
+    public Node getScene() {
+        return scene;
+    }
+
+    /**
+     * clear the cinematic of its events.
+     */
+    public void clear() {
+        dispose();
+        cinematicEvents.clear();
+        timeLine.clear();
+        if (eventsData != null) {
+            eventsData.clear();
+        }
+    }
+
+    /**
+     * used internally to cleanup the cinematic. Called when the clear() method
+     * is called
+     */
+    @Override
+    public void dispose() {
+        for (CinematicEvent event : cinematicEvents) {
+            event.dispose();
+        }
+    }
+}

+ 93 - 89
jme3-core/src/main/java/com/jme3/cinematic/KeyFrame.java

@@ -1,89 +1,93 @@
-/*
- * 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.cinematic;
-
-import com.jme3.cinematic.events.CinematicEvent;
-import com.jme3.export.*;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- *
- * @author Nehon
- */
-public class KeyFrame implements Savable {
-
-    List<CinematicEvent> cinematicEvents = new ArrayList<CinematicEvent>();
-    private int index;
-
-    public List<CinematicEvent> getCinematicEvents() {
-        return cinematicEvents;
-    }
-
-    public void setCinematicEvents(List<CinematicEvent> cinematicEvents) {
-        this.cinematicEvents = cinematicEvents;
-    }
-
-    public List<CinematicEvent> trigger() {
-        for (CinematicEvent event : cinematicEvents) {
-            event.play();
-        }
-        return cinematicEvents;
-    }
-    
-    public boolean isEmpty(){
-        return cinematicEvents.isEmpty();
-    }
-
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null);
-        oc.write(index, "index", 0);
-    }
-
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        cinematicEvents = ic.readSavableArrayList("cinematicEvents", null);
-        index=ic.readInt("index", 0);
-    }
-
-    public int getIndex() {
-        return index;
-    }
-
-    public void setIndex(int index) {
-        this.index = index;
-    }
-
-
-}
+/*
+ * 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.cinematic;
+
+import com.jme3.cinematic.events.CinematicEvent;
+import com.jme3.export.*;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Nehon
+ */
+public class KeyFrame implements Savable {
+
+    public KeyFrame(){
+        
+    }
+    
+    List<CinematicEvent> cinematicEvents = new ArrayList<>();
+    private int index;
+
+    public List<CinematicEvent> getCinematicEvents() {
+        return cinematicEvents;
+    }
+
+    public void setCinematicEvents(List<CinematicEvent> cinematicEvents) {
+        this.cinematicEvents = cinematicEvents;
+    }
+
+    public List<CinematicEvent> trigger() {
+        for (CinematicEvent event : cinematicEvents) {
+            event.play();
+        }
+        return cinematicEvents;
+    }
+    
+    public boolean isEmpty(){
+        return cinematicEvents.isEmpty();
+    }
+
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null);
+        oc.write(index, "index", 0);
+    }
+
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        cinematicEvents = ic.readSavableArrayList("cinematicEvents", null);
+        index=ic.readInt("index", 0);
+    }
+
+    public int getIndex() {
+        return index;
+    }
+
+    public void setIndex(int index) {
+        this.index = index;
+    }
+
+
+}

+ 32 - 3
jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java

@@ -41,7 +41,10 @@ import com.jme3.export.InputCapsule;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
+import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.HashMap;
@@ -80,8 +83,9 @@ public class AnimationEvent extends AbstractCinematicEvent {
      * constructors
      */
     public AnimationEvent() {
+        super();
     }
-
+    
     /**
      * creates an animation event
      *
@@ -90,6 +94,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
      */
     public AnimationEvent(Spatial model, String animationName) {
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
     }
@@ -104,6 +109,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
     public AnimationEvent(Spatial model, String animationName, float initialDuration) {
         super(initialDuration);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
     }
 
@@ -119,6 +125,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
         super(loopMode);
         initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
     }
 
@@ -134,6 +141,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
     public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode) {
         super(initialDuration, loopMode);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
     }
 
@@ -149,6 +157,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
     public AnimationEvent(Spatial model, String animationName, float initialDuration, float blendTime) {
         super(initialDuration);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         this.blendTime = blendTime;
     }
@@ -167,6 +176,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
         super(loopMode);
         initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         this.blendTime = blendTime;
     }
@@ -185,6 +195,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
     public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode, float blendTime) {
         super(initialDuration, loopMode);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         this.blendTime = blendTime;
     }
@@ -203,6 +214,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
         super(loopMode);
         initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         this.channelIndex = channelIndex;
     }
@@ -217,6 +229,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
      */
     public AnimationEvent(Spatial model, String animationName, int channelIndex) {
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
         this.channelIndex = channelIndex;
@@ -233,6 +246,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
      */
     public AnimationEvent(Spatial model, String animationName, LoopMode loopMode, int channelIndex, float blendTime) {
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         this.loopMode = loopMode;
         initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
@@ -252,6 +266,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
     public AnimationEvent(Spatial model, String animationName, float initialDuration, int channelIndex) {
         super(initialDuration);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         this.channelIndex = channelIndex;
     }
@@ -270,6 +285,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
     public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode, int channelIndex) {
         super(initialDuration, loopMode);
         this.model = model;
+        this.modelName = model.getName();
         this.animationName = animationName;
         this.channelIndex = channelIndex;
     }
@@ -299,6 +315,18 @@ public class AnimationEvent extends AbstractCinematicEvent {
                     model = cinematic.getScene().getChild(modelName);
                 }
                 if (model != null) {
+                    if(cinematic.getScene() != null){
+                        Spatial sceneModel = cinematic.getScene().getChild(model.getName());
+                        if(sceneModel != null){
+                            Node parent = sceneModel.getParent();
+                            parent.detachChild(sceneModel);
+                            sceneModel = model;
+                            parent.attachChild(sceneModel);
+                        } else {
+                            cinematic.getScene().attachChild(model);
+                        }
+                    }
+                    
                     channel = model.getControl(AnimControl.class).createChannel();
                     map.put(channelIndex, channel);
                 } else {
@@ -401,6 +429,7 @@ public class AnimationEvent extends AbstractCinematicEvent {
         OutputCapsule oc = ex.getCapsule(this);
 
         oc.write(model, "model", null);
+        oc.write(modelName, "modelName", null);
         oc.write(animationName, "animationName", "");
         oc.write(blendTime, "blendTime", 0f);
         oc.write(channelIndex, "channelIndex", 0);
@@ -411,9 +440,9 @@ public class AnimationEvent extends AbstractCinematicEvent {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
-        if (im.getFormatVersion() == 0) {
+//        if (im.getFormatVersion() == 0) {
             modelName = ic.readString("modelName", "");
-        }
+//        }
         //FIXME always the same issue, because of the clonning of assets, this won't work
         //we have to somehow store userdata in the spatial and then recurse the 
         //scene sub scenegraph to find the correct instance of the model

+ 146 - 0
jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2009-2017 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.cinematic.events;
+
+import com.jme3.app.Application;
+import com.jme3.cinematic.Cinematic;
+import com.jme3.cinematic.TimeLine;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.scene.CameraNode;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ *
+ * @author Rickard <neph1 @ github>
+ */
+public class CameraEvent extends AbstractCinematicEvent{
+
+    private String cameraName;
+    private Cinematic cinematic;
+
+    public String getCameraName() {
+        return cameraName;
+    }
+
+    public void setCameraName(String cameraName) {
+        this.cameraName = cameraName;
+    }
+
+    public CameraEvent(){
+        
+    }
+    
+    public CameraEvent(Cinematic parentEvent, String cameraName){
+        this.cinematic = parentEvent;
+        this.cameraName = cameraName;
+    }
+    
+     @Override
+    public void initEvent(Application app, Cinematic cinematic) {
+        super.initEvent(app, cinematic);
+        this.cinematic = cinematic;
+    }
+    
+    @Override
+    public void play() {
+        super.play();
+        stop();
+    }
+    
+    @Override
+    public void onPlay() {
+        cinematic.setActiveCamera(cameraName);
+    }
+
+    @Override
+    public void onUpdate(float tpf) {
+    }
+
+    @Override
+    public void onStop() {
+    }
+
+    @Override
+    public void onPause() {
+    }
+
+    @Override
+    public void forceStop() {
+    }
+
+    @Override
+    public void setTime(float time) {
+        play();
+    }
+
+    public Cinematic getCinematic() {
+        return cinematic;
+    }
+
+    public void setCinematic(Cinematic cinematic) {
+        this.cinematic = cinematic;
+    }
+    
+    
+    
+    /**
+     * used internally for serialization
+     *
+     * @param ex
+     * @throws IOException
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(cameraName, "cameraName", null);
+
+    }
+
+    /**
+     * used internally for serialization
+     *
+     * @param im
+     * @throws IOException
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        cameraName = ic.readString("cameraName", null);
+    }
+}

+ 491 - 491
jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java

@@ -1,491 +1,491 @@
-/*
- * Copyright (c) 2009-2016 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.cinematic.events;
-
-import com.jme3.animation.AnimationUtils;
-import com.jme3.animation.LoopMode;
-import com.jme3.app.Application;
-import com.jme3.cinematic.Cinematic;
-import com.jme3.cinematic.MotionPath;
-import com.jme3.cinematic.PlayState;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.math.Quaternion;
-import com.jme3.math.Vector3f;
-import com.jme3.renderer.RenderManager;
-import com.jme3.renderer.ViewPort;
-import com.jme3.scene.Spatial;
-import com.jme3.scene.control.Control;
-import com.jme3.util.clone.Cloner;
-import com.jme3.util.clone.JmeCloneable;
-import java.io.IOException;
-
-/**
- * A MotionEvent is a control over the spatial that manages the position and direction of the spatial while following a motion Path.
- *
- * You must first create a MotionPath and then create a MotionEvent to associate a spatial and the path.
- *
- * @author Nehon
- */
-public class MotionEvent extends AbstractCinematicEvent implements Control, JmeCloneable {
-
-    protected Spatial spatial;
-    protected int currentWayPoint;
-    protected float currentValue;
-    protected Vector3f direction = new Vector3f();
-    protected Vector3f lookAt = null;
-    protected Vector3f upVector = Vector3f.UNIT_Y;
-    protected Quaternion rotation = null;
-    protected Direction directionType = Direction.None;
-    protected MotionPath path;
-    private boolean isControl = true;
-    private int travelDirection = 1;
-    /**
-     * the distance traveled by the spatial on the path
-     */
-    protected float traveledDistance = 0;
-
-    /**
-     * Enum for the different type of target direction behavior.
-     */
-    public enum Direction {
-
-        /**
-         * The target stays in the starting direction.
-         */
-        None,
-        /**
-         * The target rotates with the direction of the path.
-         */
-        Path,
-        /**
-         * The target rotates with the direction of the path but with the addition of a rotation.
-         * You need to use the setRotation method when using this Direction.
-         */
-        PathAndRotation,
-        /**
-         * The target rotates with the given rotation.
-         */
-        Rotation,
-        /**
-         * The target looks at a point.
-         * You need to use the setLookAt method when using this direction.
-         */
-        LookAt
-    }
-
-    /**
-     * Create MotionEvent,
-     * when using this constructor don't forget to assign spatial and path.
-     */
-    public MotionEvent() {
-        super();
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path) {
-        super();
-        spatial.addControl(this);
-        this.path = path;
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
-        super(initialDuration);
-        spatial.addControl(this);
-        this.path = path;
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
-        super();
-        spatial.addControl(this);
-        this.path = path;
-        this.loopMode = loopMode;
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration, LoopMode loopMode) {
-        super(initialDuration);
-        spatial.addControl(this);
-        this.path = path;
-        this.loopMode = loopMode;
-    }
-
-    public void update(float tpf) {
-        if (isControl) {
-            internalUpdate(tpf);
-        }
-    }
-
-    @Override
-    public void internalUpdate(float tpf) {
-        if (playState == PlayState.Playing) {
-            time = time + (tpf * speed);
-            if (loopMode == LoopMode.Loop && time < 0) {
-                time = initialDuration;
-            }            
-            if ((time >= initialDuration || time < 0) && loopMode == LoopMode.DontLoop) {
-                if (time >= initialDuration) {
-                    path.triggerWayPointReach(path.getNbWayPoints() - 1, this);
-                }
-                stop();
-            } else {
-                time = AnimationUtils.clampWrapTime(time, initialDuration, loopMode);
-                if(time<0){
-                    speed = - speed;
-                    time = - time;
-                }
-                onUpdate(tpf);
-            }
-        }
-    }
-
-    @Override
-    public void initEvent(Application app, Cinematic cinematic) {
-        super.initEvent(app, cinematic);
-        isControl = false;
-    }
-
-    @Override
-    public void setTime(float time) {
-        super.setTime(time);
-        onUpdate(0);
-    }
-
-    public void onUpdate(float tpf) {
-        traveledDistance = path.interpolatePath(time, this, tpf);
-        computeTargetDirection();
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(lookAt, "lookAt", null);
-        oc.write(upVector, "upVector", Vector3f.UNIT_Y);
-        oc.write(rotation, "rotation", null);
-        oc.write(directionType, "directionType", Direction.None);
-        oc.write(path, "path", null);
-        oc.write(spatial, "spatial", null);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule in = im.getCapsule(this);
-        lookAt = (Vector3f) in.readSavable("lookAt", null);
-        upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
-        rotation = (Quaternion) in.readSavable("rotation", null);
-        directionType = in.readEnum("directionType", Direction.class, Direction.None);
-        path = (MotionPath) in.readSavable("path", null);
-        spatial = (Spatial) in.readSavable("spatial", null);
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     * @return
-     */
-    public boolean needsDirection() {
-        return directionType == Direction.Path || directionType == Direction.PathAndRotation;
-    }
-
-    private void computeTargetDirection() {
-        switch (directionType) {
-            case Path:
-                Quaternion q = new Quaternion();
-                q.lookAt(direction, upVector);
-                spatial.setLocalRotation(q);
-                break;
-            case LookAt:
-                if (lookAt != null) {
-                    spatial.lookAt(lookAt, upVector);
-                }
-                break;
-            case PathAndRotation:
-                if (rotation != null) {
-                    Quaternion q2 = new Quaternion();
-                    q2.lookAt(direction, upVector);
-                    q2.multLocal(rotation);
-                    spatial.setLocalRotation(q2);
-                }
-                break;
-            case Rotation:
-                if (rotation != null) {
-                    spatial.setLocalRotation(rotation);
-                }
-                break;
-            case None:
-                break;
-            default:
-                break;
-        }
-    }
-
-    /**
-     * Clone this control for the given spatial.
-     * @param spatial
-     * @return
-     */
-    @Override
-    public Control cloneForSpatial(Spatial spatial) {
-        MotionEvent control = new MotionEvent();
-        control.setPath(path);
-        control.playState = playState;
-        control.currentWayPoint = currentWayPoint;
-        control.currentValue = currentValue;
-        control.direction = direction.clone();
-        control.lookAt = lookAt;
-        control.upVector = upVector.clone();
-        control.rotation = rotation;
-        control.initialDuration = initialDuration;
-        control.speed = speed;
-        control.loopMode = loopMode;
-        control.directionType = directionType;
-
-        return control;
-    }
-
-    @Override   
-    public Object jmeClone() {
-        MotionEvent control = new MotionEvent();
-        control.path = path;
-        control.playState = playState;
-        control.currentWayPoint = currentWayPoint;
-        control.currentValue = currentValue;
-        control.direction = direction.clone();
-        control.lookAt = lookAt;
-        control.upVector = upVector.clone();
-        control.rotation = rotation;
-        control.initialDuration = initialDuration;
-        control.speed = speed;
-        control.loopMode = loopMode;
-        control.directionType = directionType;
-        control.spatial = spatial;
-
-        return control;
-    }     
-
-    @Override   
-    public void cloneFields( Cloner cloner, Object original ) { 
-        this.spatial = cloner.clone(spatial);
-    }
-         
-    @Override
-    public void onPlay() {
-        traveledDistance = 0;
-    }
-
-    @Override
-    public void onStop() {
-        currentWayPoint = 0;
-    }
-
-    @Override
-    public void onPause() {
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     * @return
-     */
-    public float getCurrentValue() {
-        return currentValue;
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     *
-     */
-    public void setCurrentValue(float currentValue) {
-        this.currentValue = currentValue;
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     * @return
-     */
-    public int getCurrentWayPoint() {
-        return currentWayPoint;
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     *
-     */
-    public void setCurrentWayPoint(int currentWayPoint) {
-        this.currentWayPoint = currentWayPoint;
-    }
-
-    /**
-     * Returns the direction the spatial is moving.
-     * @return
-     */
-    public Vector3f getDirection() {
-        return direction;
-    }
-
-    /**
-     * Sets the direction of the spatial, using the Y axis as the up vector.
-     * Use MotionEvent#setDirection((Vector3f direction,Vector3f upVector) if 
-     * you want a custum up vector.
-     * This method is used by the motion path.
-     * @param direction
-     */
-    public void setDirection(Vector3f direction) {
-        setDirection(direction, Vector3f.UNIT_Y); 
-   }
-    
-    /**
-     * Sets the direction of the spatial with the given up vector.
-     * This method is used by the motion path.
-     * @param direction
-     * @param upVector the up vector to consider for this direction.
-     */
-    public void setDirection(Vector3f direction,Vector3f upVector) {
-        this.direction.set(direction);
-        this.upVector.set(upVector);
-    }
-
-    /**
-     * Returns the direction type of the target.
-     * @return the direction type.
-     */
-    public Direction getDirectionType() {
-        return directionType;
-    }
-
-    /**
-     * Sets the direction type of the target.
-     * On each update the direction given to the target can have different behavior.
-     * See the Direction Enum for explanations.
-     * @param directionType the direction type.
-     */
-    public void setDirectionType(Direction directionType) {
-        this.directionType = directionType;
-    }
-
-    /**
-     * Set the lookAt for the target.
-     * This can be used only if direction Type is Direction.LookAt.
-     * @param lookAt the position to look at.
-     * @param upVector the up vector.
-     */
-    public void setLookAt(Vector3f lookAt, Vector3f upVector) {
-        this.lookAt = lookAt;
-        this.upVector = upVector;
-    }
-
-    /**
-     * Returns the rotation of the target.
-     * @return the rotation quaternion.
-     */
-    public Quaternion getRotation() {
-        return rotation;
-    }
-
-    /**
-     * Sets the rotation of the target.
-     * This can be used only if direction Type is Direction.PathAndRotation or Direction.Rotation.
-     * With PathAndRotation the target will face the direction of the path multiplied by the given Quaternion.
-     * With Rotation the rotation of the target will be set with the given Quaternion.
-     * @param rotation the rotation quaternion.
-     */
-    public void setRotation(Quaternion rotation) {
-        this.rotation = rotation;
-    }
-
-    /**
-     * Return the motion path this control follows.
-     * @return
-     */
-    public MotionPath getPath() {
-        return path;
-    }
-
-    /**
-     * Sets the motion path to follow.
-     * @param path
-     */
-    public void setPath(MotionPath path) {
-        this.path = path;
-    }
-
-    public void setEnabled(boolean enabled) {
-        if (enabled) {
-            play();
-        } else {
-            pause();
-        }
-    }
-
-    public boolean isEnabled() {
-        return playState != PlayState.Stopped;
-    }
-
-    public void render(RenderManager rm, ViewPort vp) {
-    }
-
-    public void setSpatial(Spatial spatial) {
-        this.spatial = spatial;
-    }
-
-    public Spatial getSpatial() {
-        return spatial;
-    }
-
-    /**
-     * Return the distance traveled by the spatial on the path.
-     * @return 
-     */
-    public float getTraveledDistance() {
-        return traveledDistance;
-    }
-}
+/*
+ * Copyright (c) 2009-2016 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.cinematic.events;
+
+import com.jme3.animation.AnimationUtils;
+import com.jme3.animation.LoopMode;
+import com.jme3.app.Application;
+import com.jme3.cinematic.Cinematic;
+import com.jme3.cinematic.MotionPath;
+import com.jme3.cinematic.PlayState;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+import java.io.IOException;
+
+/**
+ * A MotionEvent is a control over the spatial that manages the position and direction of the spatial while following a motion Path.
+ *
+ * You must first create a MotionPath and then create a MotionEvent to associate a spatial and the path.
+ *
+ * @author Nehon
+ */
+public class MotionEvent extends AbstractCinematicEvent implements Control, JmeCloneable {
+
+    protected Spatial spatial;
+    protected int currentWayPoint;
+    protected float currentValue;
+    protected Vector3f direction = new Vector3f();
+    protected Vector3f lookAt = null;
+    protected Vector3f upVector = Vector3f.UNIT_Y;
+    protected Quaternion rotation = null;
+    protected Direction directionType = Direction.None;
+    protected MotionPath path;
+    private boolean isControl = true;
+    private int travelDirection = 1;
+    /**
+     * the distance traveled by the spatial on the path
+     */
+    protected float traveledDistance = 0;
+
+    /**
+     * Enum for the different type of target direction behavior.
+     */
+    public enum Direction {
+
+        /**
+         * The target stays in the starting direction.
+         */
+        None,
+        /**
+         * The target rotates with the direction of the path.
+         */
+        Path,
+        /**
+         * The target rotates with the direction of the path but with the addition of a rotation.
+         * You need to use the setRotation method when using this Direction.
+         */
+        PathAndRotation,
+        /**
+         * The target rotates with the given rotation.
+         */
+        Rotation,
+        /**
+         * The target looks at a point.
+         * You need to use the setLookAt method when using this direction.
+         */
+        LookAt
+    }
+
+    /**
+     * Create MotionEvent,
+     * when using this constructor don't forget to assign spatial and path.
+     */
+    public MotionEvent() {
+        super();
+    }
+    
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path) {
+        super();
+        spatial.addControl(this);
+        this.path = path;
+    }
+
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
+        super(initialDuration);
+        spatial.addControl(this);
+        this.path = path;
+    }
+
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
+        super();
+        spatial.addControl(this);
+        this.path = path;
+        this.loopMode = loopMode;
+    }
+
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration, LoopMode loopMode) {
+        super(initialDuration);
+        spatial.addControl(this);
+        this.path = path;
+        this.loopMode = loopMode;
+    }
+
+    public void update(float tpf) {
+        if (isControl) {
+            internalUpdate(tpf);
+        }
+    }
+
+    @Override
+    public void internalUpdate(float tpf) {
+        if (playState == PlayState.Playing) {
+            time = time + (tpf * speed);
+            if (loopMode == LoopMode.Loop && time < 0) {
+                time = initialDuration;
+            }            
+            if ((time >= initialDuration || time < 0) && loopMode == LoopMode.DontLoop) {
+                if (time >= initialDuration) {
+                    path.triggerWayPointReach(path.getNbWayPoints() - 1, this);
+                }
+                stop();
+            } else {
+                time = AnimationUtils.clampWrapTime(time, initialDuration, loopMode);
+                if(time<0){
+                    speed = - speed;
+                    time = - time;
+                }
+                onUpdate(tpf);
+            }
+        }
+    }
+
+    @Override
+    public void initEvent(Application app, Cinematic cinematic) {
+        super.initEvent(app, cinematic);
+        isControl = false;
+    }
+
+    @Override
+    public void setTime(float time) {
+        super.setTime(time);
+        onUpdate(0);
+    }
+
+    public void onUpdate(float tpf) {
+        traveledDistance = path.interpolatePath(time, this, tpf);
+        computeTargetDirection();
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(lookAt, "lookAt", null);
+        oc.write(upVector, "upVector", Vector3f.UNIT_Y);
+        oc.write(rotation, "rotation", null);
+        oc.write(directionType, "directionType", Direction.None);
+        oc.write(path, "path", null);
+        oc.write(spatial, "spatial", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule in = im.getCapsule(this);
+        lookAt = (Vector3f) in.readSavable("lookAt", null);
+        upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
+        rotation = (Quaternion) in.readSavable("rotation", null);
+        directionType = in.readEnum("directionType", Direction.class, Direction.None);
+        path = (MotionPath) in.readSavable("path", null);
+        spatial = (Spatial) in.readSavable("spatial", null);
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     * @return
+     */
+    public boolean needsDirection() {
+        return directionType == Direction.Path || directionType == Direction.PathAndRotation;
+    }
+
+    private void computeTargetDirection() {
+        switch (directionType) {
+            case Path:
+                Quaternion q = new Quaternion();
+                q.lookAt(direction, upVector);
+                spatial.setLocalRotation(q);
+                break;
+            case LookAt:
+                if (lookAt != null) {
+                    spatial.lookAt(lookAt, upVector);
+                }
+                break;
+            case PathAndRotation:
+                if (rotation != null) {
+                    Quaternion q2 = new Quaternion();
+                    q2.lookAt(direction, upVector);
+                    q2.multLocal(rotation);
+                    spatial.setLocalRotation(q2);
+                }
+                break;
+            case Rotation:
+                if (rotation != null) {
+                    spatial.setLocalRotation(rotation);
+                }
+                break;
+            case None:
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Clone this control for the given spatial.
+     * @param spatial
+     * @return
+     */
+    @Override
+    public Control cloneForSpatial(Spatial spatial) {
+        MotionEvent control = new MotionEvent();
+        control.setPath(path);
+        control.playState = playState;
+        control.currentWayPoint = currentWayPoint;
+        control.currentValue = currentValue;
+        control.direction = direction.clone();
+        control.lookAt = lookAt;
+        control.upVector = upVector.clone();
+        control.rotation = rotation;
+        control.initialDuration = initialDuration;
+        control.speed = speed;
+        control.loopMode = loopMode;
+        control.directionType = directionType;
+
+        return control;
+    }
+
+    @Override   
+    public Object jmeClone() {
+        MotionEvent control = new MotionEvent();
+        control.path = path;
+        control.playState = playState;
+        control.currentWayPoint = currentWayPoint;
+        control.currentValue = currentValue;
+        control.direction = direction.clone();
+        control.lookAt = lookAt;
+        control.upVector = upVector.clone();
+        control.rotation = rotation;
+        control.initialDuration = initialDuration;
+        control.speed = speed;
+        control.loopMode = loopMode;
+        control.directionType = directionType;
+        control.spatial = spatial;
+
+        return control;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+    }
+         
+    @Override
+    public void onPlay() {
+        traveledDistance = 0;
+    }
+
+    @Override
+    public void onStop() {
+        currentWayPoint = 0;
+    }
+
+    @Override
+    public void onPause() {
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     * @return
+     */
+    public float getCurrentValue() {
+        return currentValue;
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     *
+     */
+    public void setCurrentValue(float currentValue) {
+        this.currentValue = currentValue;
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     * @return
+     */
+    public int getCurrentWayPoint() {
+        return currentWayPoint;
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     *
+     */
+    public void setCurrentWayPoint(int currentWayPoint) {
+        this.currentWayPoint = currentWayPoint;
+    }
+
+    /**
+     * Returns the direction the spatial is moving.
+     * @return
+     */
+    public Vector3f getDirection() {
+        return direction;
+    }
+
+    /**
+     * Sets the direction of the spatial, using the Y axis as the up vector.
+     * Use MotionEvent#setDirection((Vector3f direction,Vector3f upVector) if 
+     * you want a custum up vector.
+     * This method is used by the motion path.
+     * @param direction
+     */
+    public void setDirection(Vector3f direction) {
+        setDirection(direction, Vector3f.UNIT_Y); 
+   }
+    
+    /**
+     * Sets the direction of the spatial with the given up vector.
+     * This method is used by the motion path.
+     * @param direction
+     * @param upVector the up vector to consider for this direction.
+     */
+    public void setDirection(Vector3f direction,Vector3f upVector) {
+        this.direction.set(direction);
+        this.upVector.set(upVector);
+    }
+
+    /**
+     * Returns the direction type of the target.
+     * @return the direction type.
+     */
+    public Direction getDirectionType() {
+        return directionType;
+    }
+
+    /**
+     * Sets the direction type of the target.
+     * On each update the direction given to the target can have different behavior.
+     * See the Direction Enum for explanations.
+     * @param directionType the direction type.
+     */
+    public void setDirectionType(Direction directionType) {
+        this.directionType = directionType;
+    }
+
+    /**
+     * Set the lookAt for the target.
+     * This can be used only if direction Type is Direction.LookAt.
+     * @param lookAt the position to look at.
+     * @param upVector the up vector.
+     */
+    public void setLookAt(Vector3f lookAt, Vector3f upVector) {
+        this.lookAt = lookAt;
+        this.upVector = upVector;
+    }
+
+    /**
+     * Returns the rotation of the target.
+     * @return the rotation quaternion.
+     */
+    public Quaternion getRotation() {
+        return rotation;
+    }
+
+    /**
+     * Sets the rotation of the target.
+     * This can be used only if direction Type is Direction.PathAndRotation or Direction.Rotation.
+     * With PathAndRotation the target will face the direction of the path multiplied by the given Quaternion.
+     * With Rotation the rotation of the target will be set with the given Quaternion.
+     * @param rotation the rotation quaternion.
+     */
+    public void setRotation(Quaternion rotation) {
+        this.rotation = rotation;
+    }
+
+    /**
+     * Return the motion path this control follows.
+     * @return
+     */
+    public MotionPath getPath() {
+        return path;
+    }
+
+    /**
+     * Sets the motion path to follow.
+     * @param path
+     */
+    public void setPath(MotionPath path) {
+        this.path = path;
+    }
+
+    public void setEnabled(boolean enabled) {
+        if (enabled) {
+            play();
+        } else {
+            pause();
+        }
+    }
+
+    public boolean isEnabled() {
+        return playState != PlayState.Stopped;
+    }
+
+    public void render(RenderManager rm, ViewPort vp) {
+    }
+
+    public void setSpatial(Spatial spatial) {
+        this.spatial = spatial;
+    }
+
+    public Spatial getSpatial() {
+        return spatial;
+    }
+
+    /**
+     * Return the distance traveled by the spatial on the path.
+     * @return 
+     */
+    public float getTraveledDistance() {
+        return traveledDistance;
+    }
+}

+ 230 - 229
jme3-core/src/main/java/com/jme3/cinematic/events/SoundEvent.java

@@ -1,229 +1,230 @@
-/*
- * 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.cinematic.events;
-
-import com.jme3.animation.LoopMode;
-import com.jme3.app.Application;
-import com.jme3.audio.AudioNode;
-import com.jme3.audio.AudioSource;
-import com.jme3.cinematic.Cinematic;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import java.io.IOException;
-
-/**
- * A sound track to be played in a cinematic.
- * @author Nehon
- */
-public class SoundEvent extends AbstractCinematicEvent {
-
-    protected String path;
-    protected AudioNode audioNode;
-    protected boolean stream = false;
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     */
-    public SoundEvent(String path) {
-        this.path = path;
-    }
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     */
-    public SoundEvent(String path, boolean stream) {
-        this(path);
-        this.stream = stream;
-    }
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     * @param initialDuration the initial duration of the event
-     */
-    public SoundEvent(String path, boolean stream, float initialDuration) {
-        super(initialDuration);
-        this.path = path;
-        this.stream = stream;
-    }
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, boolean stream, LoopMode loopMode) {
-        super(loopMode);
-        this.path = path;
-        this.stream = stream;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     * @param initialDuration the initial duration of the event
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, boolean stream, float initialDuration, LoopMode loopMode) {
-        super(initialDuration, loopMode);
-        this.path = path;
-        this.stream = stream;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
-     * @param initialDuration the initial duration of the event
-     */
-    public SoundEvent(String path, float initialDuration) {
-        super(initialDuration);
-        this.path = path;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")   
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, LoopMode loopMode) {
-        super(loopMode);
-        this.path = path;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
-     * @param initialDuration the initial duration of the event
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, float initialDuration, LoopMode loopMode) {
-        super(initialDuration, loopMode);
-        this.path = path;
-    }
-
-    /**
-     * creates a sound event
-     * used for serialization
-     */
-    public SoundEvent() {
-    }
-
-    @Override
-    public void initEvent(Application app, Cinematic cinematic) {
-        super.initEvent(app, cinematic);
-        audioNode = new AudioNode(app.getAssetManager(), path, stream);
-        audioNode.setPositional(false);
-        setLoopMode(loopMode);
-    }
-
-    @Override
-    public void setTime(float time) {
-        super.setTime(time);
-        //can occur on rewind
-        if (time < 0f) {            
-            stop();
-        }else{
-            audioNode.setTimeOffset(time);
-        }
-    }
-
-    @Override
-    public void onPlay() {
-        audioNode.play();
-    }
-
-    @Override
-    public void onStop() {
-        audioNode.stop();
-
-    }
-
-    @Override
-    public void onPause() {
-        audioNode.pause();
-    }
-
-    @Override
-    public void onUpdate(float tpf) {
-        if (audioNode.getStatus() == AudioSource.Status.Stopped) {
-            stop();
-        }
-    }
-
-    /**
-     *  Returns the underlying audio node of this sound track
-     * @return
-     */
-    public AudioNode getAudioNode() {
-        return audioNode;
-    }
-
-    @Override
-    public void setLoopMode(LoopMode loopMode) {
-        super.setLoopMode(loopMode);
-
-        if (loopMode != LoopMode.DontLoop) {
-            audioNode.setLooping(true);
-        } else {
-            audioNode.setLooping(false);
-        }
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(path, "path", "");
-        oc.write(stream, "stream", false);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-        path = ic.readString("path", "");
-        stream = ic.readBoolean("stream", false);
-
-    }
-}
+/*
+ * 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.cinematic.events;
+
+import com.jme3.animation.LoopMode;
+import com.jme3.app.Application;
+import com.jme3.audio.AudioNode;
+import com.jme3.audio.AudioSource;
+import com.jme3.cinematic.Cinematic;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import java.io.IOException;
+
+/**
+ * A sound track to be played in a cinematic.
+ * @author Nehon
+ */
+public class SoundEvent extends AbstractCinematicEvent {
+
+    protected String path;
+    protected AudioNode audioNode;
+    protected boolean stream = false;
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     */
+    public SoundEvent(String path) {
+        this.path = path;
+    }
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     */
+    public SoundEvent(String path, boolean stream) {
+        this(path);
+        this.stream = stream;
+    }
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     * @param initialDuration the initial duration of the event
+     */
+    public SoundEvent(String path, boolean stream, float initialDuration) {
+        super(initialDuration);
+        this.path = path;
+        this.stream = stream;
+    }
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, boolean stream, LoopMode loopMode) {
+        super(loopMode);
+        this.path = path;
+        this.stream = stream;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     * @param initialDuration the initial duration of the event
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, boolean stream, float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+        this.path = path;
+        this.stream = stream;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
+     * @param initialDuration the initial duration of the event
+     */
+    public SoundEvent(String path, float initialDuration) {
+        super(initialDuration);
+        this.path = path;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")   
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, LoopMode loopMode) {
+        super(loopMode);
+        this.path = path;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
+     * @param initialDuration the initial duration of the event
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+        this.path = path;
+    }
+
+    /**
+     * creates a sound event
+     * used for serialization
+     */
+    public SoundEvent() {
+        super();
+    }
+
+    @Override
+    public void initEvent(Application app, Cinematic cinematic) {
+        super.initEvent(app, cinematic);
+        audioNode = new AudioNode(app.getAssetManager(), path, stream);
+        audioNode.setPositional(false);
+        setLoopMode(loopMode);
+    }
+
+    @Override
+    public void setTime(float time) {
+        super.setTime(time);
+        //can occur on rewind
+        if (time < 0f) {            
+            stop();
+        }else{
+            audioNode.setTimeOffset(time);
+        }
+    }
+
+    @Override
+    public void onPlay() {
+        audioNode.play();
+    }
+
+    @Override
+    public void onStop() {
+        audioNode.stop();
+
+    }
+
+    @Override
+    public void onPause() {
+        audioNode.pause();
+    }
+
+    @Override
+    public void onUpdate(float tpf) {
+        if (audioNode.getStatus() == AudioSource.Status.Stopped) {
+            stop();
+        }
+    }
+
+    /**
+     *  Returns the underlying audio node of this sound track
+     * @return
+     */
+    public AudioNode getAudioNode() {
+        return audioNode;
+    }
+
+    @Override
+    public void setLoopMode(LoopMode loopMode) {
+        super.setLoopMode(loopMode);
+
+        if (loopMode != LoopMode.DontLoop) {
+            audioNode.setLooping(true);
+        } else {
+            audioNode.setLooping(false);
+        }
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(path, "path", "");
+        oc.write(stream, "stream", false);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        path = ic.readString("path", "");
+        stream = ic.readBoolean("stream", false);
+
+    }
+}

+ 1 - 1
jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java

@@ -1016,7 +1016,7 @@ public class ParticleEmitter extends Geometry {
         particles[idx2] = p1;
     }
 
-    private void updateParticle(Particle p, float tpf, Vector3f min, Vector3f max){
+    protected void updateParticle(Particle p, float tpf, Vector3f min, Vector3f max){
         // applying gravity
         p.velocity.x -= gravity.x * tpf;
         p.velocity.y -= gravity.y * tpf;

+ 39 - 4
jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java

@@ -48,6 +48,7 @@ import com.jme3.texture.Texture2D;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.MipMapGenerator;
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -72,6 +73,8 @@ public class EnvironmentCamera extends BaseAppState {
 
     protected Image.Format imageFormat = Image.Format.RGB16F;
 
+    public TextureCubeMap debugEnv;
+
     //Axis for cameras
     static {
         //PositiveX axis(left, up, direction)
@@ -108,7 +111,10 @@ public class EnvironmentCamera extends BaseAppState {
     protected Vector3f position = new Vector3f();
     protected ColorRGBA backGroundColor;
 
-    protected int size = 128;
+    /**
+     * The size of environment cameras.
+     */
+    protected int size = 256;
 
     private final List<SnapshotJob> jobs = new ArrayList<SnapshotJob>();
 
@@ -185,19 +191,48 @@ public class EnvironmentCamera extends BaseAppState {
             buffers[i] = BufferUtils.createByteBuffer(size * size * imageFormat.getBitsPerPixel() / 8);
             renderManager.getRenderer().readFrameBufferWithFormat(framebuffers[i], buffers[i], imageFormat);
             images[i] = new Image(imageFormat, size, size, buffers[i], ColorSpace.Linear);
+            MipMapGenerator.generateMipMaps(images[i]);
         }
 
         final TextureCubeMap map = EnvMapUtils.makeCubeMap(images[0], images[1], images[2], images[3], images[4], images[5], imageFormat);
-
+            debugEnv = map;
         job.callback.done(map);
         map.getImage().dispose();
         jobs.remove(0);
     }
 
+    /**
+     * Gets the size of environment cameras.
+     *
+     * @return the size of environment cameras.
+     */
     public int getSize() {
         return size;
     }
 
+    /**
+     * Sets the size of environment cameras and rebuild this state if it was initialized.
+     *
+     * @param size the size of environment cameras.
+     */
+    public void setSize(final int size) {
+        this.size = size;
+        rebuild();
+    }
+
+    /**
+     * Rebuild all environment cameras.
+     */
+    protected void rebuild() {
+
+        if (!isInitialized()) {
+            return;
+        }
+
+        cleanup(getApplication());
+        initialize(getApplication());
+    }
+
     public Vector3f getPosition() {
         return position;
     }
@@ -224,8 +259,7 @@ public class EnvironmentCamera extends BaseAppState {
         this.backGroundColor = app.getViewPort().getBackgroundColor();
 
         final Camera[] cameras = new Camera[6];
-
-        Texture2D[] textures = new Texture2D[6];
+        final Texture2D[] textures = new Texture2D[6];
 
         viewports = new ViewPort[6];
         framebuffers = new FrameBuffer[6];
@@ -241,6 +275,7 @@ public class EnvironmentCamera extends BaseAppState {
         }
     }
 
+
     @Override
     protected void cleanup(Application app) {
         this.backGroundColor = null;

+ 44 - 40
jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java

@@ -31,16 +31,14 @@
  */
 package com.jme3.environment;
 
-import com.jme3.light.LightProbe;
-import com.jme3.environment.generation.JobProgressListener;
-import com.jme3.environment.generation.PrefilteredEnvMapFaceGenerator;
-import com.jme3.environment.generation.IrradianceMapGenerator;
-import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.environment.generation.JobProgressAdapter;
 import com.jme3.app.Application;
+import com.jme3.environment.generation.*;
+import com.jme3.environment.util.EnvMapUtils;
+import com.jme3.light.LightProbe;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.texture.TextureCubeMap;
+
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 
 /**
@@ -109,94 +107,99 @@ public class LightProbeFactory {
      
      * @param envCam the EnvironmentCamera
      * @param scene the Scene
+     * @param genType Fast or HighQuality. Fast may be ok for many types of environment, but you may need high quality when an environment map has very high lighting values.
      * @param listener the listener of the genration progress.
      * @return the created LightProbe
      */
-    public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+    public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final EnvMapUtils.GenerationType genType, final JobProgressListener<LightProbe> listener) {
         final LightProbe probe = new LightProbe();
         probe.setPosition(envCam.getPosition());
-        probe.setIrradianceMap(EnvMapUtils.createIrradianceMap(envCam.getSize(), envCam.getImageFormat()));
         probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat()));
         envCam.snapshot(scene, new JobProgressAdapter<TextureCubeMap>() {
 
             @Override
             public void done(TextureCubeMap map) {
-                generatePbrMaps(map, probe, envCam.getApplication(), listener);
+                generatePbrMaps(map, probe, envCam.getApplication(), genType, listener);
             }
         });
         return probe;
     }
-    
-     /**
-     * Updates a LightProbe with the giver EnvironmentCamera in the given scene.
-     * 
+
+    public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+        return makeProbe(envCam, scene, EnvMapUtils.GenerationType.Fast, listener);
+    }
+
+    /**
+     * Updates a LightProbe with the given EnvironmentCamera in the given scene.
+     * <p>
      * Note that this is an assynchronous process that will run on multiple threads.
      * The process is thread safe.
      * The created lightProbe will only be marked as ready when the rendering process is done.
-     *      
-     * The JobProgressListener will be notified of the progress of the generation. 
-     * Note that you can also use a {@link JobProgressAdapter}. 
-     *      
+     * <p>
+     * The JobProgressListener will be notified of the progress of the generation.
+     * Note that you can also use a {@link JobProgressAdapter}.
+     *
+     * @param probe    the Light probe to update
+     * @param envCam   the EnvironmentCamera
+     * @param scene    the Scene
+     * @param genType  Fast or HighQuality. Fast may be ok for many types of environment, but you may need high quality when an environment map has very high lighting values.
+     * @param listener the listener of the genration progress.
+     * @return the created LightProbe
      * @see LightProbe
      * @see EnvironmentCamera
      * @see JobProgressListener
-     * 
-     * @param probe the Light probe to update
-     * @param envCam the EnvironmentCamera
-     * @param scene the Scene
-     * @param listener the listener of the genration progress.
-     * @return the created LightProbe
      */
-    public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+    public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCamera envCam, Spatial scene, final EnvMapUtils.GenerationType genType, final JobProgressListener<LightProbe> listener) {
         
         envCam.setPosition(probe.getPosition());
         
         probe.setReady(false);
-        
-        if(probe.getIrradianceMap() != null) {
-            probe.getIrradianceMap().getImage().dispose();
+
+        if (probe.getPrefilteredEnvMap() != null) {
             probe.getPrefilteredEnvMap().getImage().dispose();
         }
-        
-        probe.setIrradianceMap(EnvMapUtils.createIrradianceMap(envCam.getSize(), envCam.getImageFormat()));
+
         probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat()));
-        
-        
+
         envCam.snapshot(scene, new JobProgressAdapter<TextureCubeMap>() {
 
             @Override
             public void done(TextureCubeMap map) {
-                generatePbrMaps(map, probe, envCam.getApplication(), listener);
+                generatePbrMaps(map, probe, envCam.getApplication(), genType, listener);
             }
         });
         return probe;
     }
 
+    public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+        return updateProbe(probe, envCam, scene, EnvMapUtils.GenerationType.Fast, listener);
+    }
+
     /**
      * Internally called to generate the maps.
-     * This method will spawn 7 thread (one for the IrradianceMap, and one for each face of the prefiltered env map).
+     * This method will spawn 7 thread (one for the Irradiance spherical harmonics generator, and one for each face of the prefiltered env map).
      * Those threads will be executed in a ScheduledThreadPoolExecutor that will be shutdown when the genration is done.
-     * 
+     *
      * @param envMap the raw env map rendered by the env camera
      * @param probe the LigthProbe to generate maps for
      * @param app the Application
      * @param listener a progress listener. (can be null if no progress reporting is needed)
      */
-    private static void generatePbrMaps(TextureCubeMap envMap, final LightProbe probe, Application app, final JobProgressListener<LightProbe> listener) {
-        IrradianceMapGenerator irrMapGenerator;
+    private static void generatePbrMaps(TextureCubeMap envMap, final LightProbe probe, Application app, EnvMapUtils.GenerationType genType, final JobProgressListener<LightProbe> listener) {
+        IrradianceSphericalHarmonicsGenerator irrShGenerator;
         PrefilteredEnvMapFaceGenerator[] pemGenerators = new PrefilteredEnvMapFaceGenerator[6];
 
         final JobState jobState = new JobState(new ScheduledThreadPoolExecutor(7));
 
-        irrMapGenerator = new IrradianceMapGenerator(app, new JobListener(listener, jobState, probe, 6));
+        irrShGenerator = new IrradianceSphericalHarmonicsGenerator(app, new JobListener(listener, jobState, probe, 6));
         int size = envMap.getImage().getWidth();
-        irrMapGenerator.setGenerationParam(EnvMapUtils.duplicateCubeMap(envMap), size, EnvMapUtils.FixSeamsMethod.Wrap, probe.getIrradianceMap());
+        irrShGenerator.setGenerationParam(EnvMapUtils.duplicateCubeMap(envMap), probe);
 
-        jobState.executor.execute(irrMapGenerator);
+        jobState.executor.execute(irrShGenerator);
 
         for (int i = 0; i < pemGenerators.length; i++) {
             pemGenerators[i] = new PrefilteredEnvMapFaceGenerator(app, i, new JobListener(listener, jobState, probe, i));
-            pemGenerators[i].setGenerationParam(EnvMapUtils.duplicateCubeMap(envMap), size, EnvMapUtils.FixSeamsMethod.Wrap, probe.getPrefilteredEnvMap());
+            pemGenerators[i].setGenerationParam(EnvMapUtils.duplicateCubeMap(envMap), size, EnvMapUtils.FixSeamsMethod.None, genType, probe.getPrefilteredEnvMap());
             jobState.executor.execute(pemGenerators[i]);
         }
     }
@@ -280,6 +283,7 @@ public class LightProbeFactory {
 
             jobState.done[index] = true;
             if (jobState.isDone()) {
+                probe.setNbMipMaps(probe.getPrefilteredEnvMap().getImage().getMipMapSizes().length);
                 probe.setReady(true);
                 if (globalListener != null) {
                     globalListener.done(probe);

+ 0 - 176
jme3-core/src/main/java/com/jme3/environment/generation/IrradianceMapGenerator.java

@@ -1,176 +0,0 @@
-/*
- * Copyright (c) 2009-2015 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.environment.generation;
-
-import com.jme3.environment.util.CubeMapWrapper;
-import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.app.Application;
-import com.jme3.math.ColorRGBA;
-import com.jme3.math.Vector3f;
-import com.jme3.texture.TextureCubeMap;
-import static com.jme3.environment.util.EnvMapUtils.shBandFactor;
-import com.jme3.util.BufferUtils;
-import java.nio.ByteBuffer;
-import java.util.concurrent.Callable;
-
-/**
- *
- * Generates the Irrafiance map for PBR. This job can be lauched from a separate
- * thread.
- *
- * TODO there is a lot of duplicate code here with the EnvMapUtils.
- *
- * @author Nehon
- */
-//TODO there is a lot of duplicate code here with the EnvMapUtils. We should, 
-//either leverage the code from the util class either remove it and only allow 
-//parallel generation using this runnable.
-public class IrradianceMapGenerator extends RunnableWithProgress {
-
-    private int targetMapSize;
-    private EnvMapUtils.FixSeamsMethod fixSeamsMethod;
-    private TextureCubeMap sourceMap;
-    private TextureCubeMap store;
-    private final Application app;
-
-    /**
-     * Creates an Irradiance map generator. The app is needed to enqueue the
-     * call to the EnvironmentCamera when the generation is done, so that this
-     * process is thread safe.
-     *
-     * @param app the Application
-     * @param listener
-     */
-    public IrradianceMapGenerator(Application app, JobProgressListener<Integer> listener) {
-        super(listener);
-        this.app = app;
-    }
-
-    /**
-     * Fills all the genration parameters
-     *
-     * @param sourceMap the source cube map
-     * @param targetMapSize the size of the generated map (width or height in
-     * pixel)
-     * @param fixSeamsMethod the method used to fix seams as described here
-     * {@link EnvMapUtils.FixSeamsMethod}
-     *
-     * @param store The cube map to store the result in.
-     */
-    public void setGenerationParam(TextureCubeMap sourceMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
-        this.sourceMap = sourceMap;
-        this.targetMapSize = targetMapSize;
-        this.fixSeamsMethod = fixSeamsMethod;
-        this.store = store;
-        reset();
-    }
-
-    @Override
-    public void run() {
-        app.enqueue(new Callable<Void>() {
-
-            @Override
-            public Void call() throws Exception {
-                listener.start();
-                return null;
-            }
-        });
-        try {
-            Vector3f[] shCoeffs = EnvMapUtils.getSphericalHarmonicsCoefficents(sourceMap);
-            store = generateIrradianceMap(shCoeffs, targetMapSize, fixSeamsMethod, store);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        app.enqueue(new Callable<Void>() {
-
-            @Override
-            public Void call() throws Exception {
-                listener.done(6);
-                return null;
-            }
-        });
-    }
-
-    /**
-     * Generates the Irradiance map (used for image based difuse lighting) from
-     * Spherical Harmonics coefficients previously computed with
-     * {@link EnvMapUtils#getSphericalHarmonicsCoefficents(com.jme3.texture.TextureCubeMap)}
-     *
-     * @param shCoeffs the SH coeffs
-     * @param targetMapSize the size of the irradiance map to generate
-     * @param fixSeamsMethod the method to fix seams
-     * @param store
-     * @return The irradiance cube map for the given coefficients
-     */
-    public TextureCubeMap generateIrradianceMap(Vector3f[] shCoeffs, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
-        TextureCubeMap irrCubeMap = store;
-
-        setEnd(6 + 6);
-        for (int i = 0; i < 6; i++) {
-            ByteBuffer buf = BufferUtils.createByteBuffer(targetMapSize * targetMapSize * store.getImage().getFormat().getBitsPerPixel() / 8);
-            irrCubeMap.getImage().setData(i, buf);
-            progress();
-        }
-
-        Vector3f texelVect = new Vector3f();
-        ColorRGBA color = new ColorRGBA(ColorRGBA.Black);
-        float[] shDir = new float[9];
-        CubeMapWrapper envMapWriter = new CubeMapWrapper(irrCubeMap);
-        for (int face = 0; face < 6; face++) {
-
-            for (int y = 0; y < targetMapSize; y++) {
-                for (int x = 0; x < targetMapSize; x++) {
-                    EnvMapUtils.getVectorFromCubemapFaceTexCoord(x, y, targetMapSize, face, texelVect, fixSeamsMethod);
-                    EnvMapUtils.evalShBasis(texelVect, shDir);
-                    color.set(0, 0, 0, 0);
-                    for (int i = 0; i < EnvMapUtils.NUM_SH_COEFFICIENT; i++) {
-                        color.set(color.r + shCoeffs[i].x * shDir[i] * shBandFactor[i],
-                                color.g + shCoeffs[i].y * shDir[i] * shBandFactor[i],
-                                color.b + shCoeffs[i].z * shDir[i] * shBandFactor[i],
-                                1.0f);
-                    }
-
-                    //clamping the color because very low value close to zero produce artifacts
-                    color.r = Math.max(0.0001f, color.r);
-                    color.g = Math.max(0.0001f, color.g);
-                    color.b = Math.max(0.0001f, color.b);
-
-                    envMapWriter.setPixel(x, y, face, color);
-                    
-                }
-            }
-            progress();
-        }
-        return irrCubeMap;
-    }
-
-}

+ 117 - 0
jme3-core/src/main/java/com/jme3/environment/generation/IrradianceSphericalHarmonicsGenerator.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.environment.generation;
+
+import com.jme3.app.Application;
+import com.jme3.environment.util.CubeMapWrapper;
+import com.jme3.environment.util.EnvMapUtils;
+import com.jme3.light.LightProbe;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.texture.TextureCubeMap;
+import com.jme3.util.BufferUtils;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Callable;
+
+import static com.jme3.environment.util.EnvMapUtils.shBandFactor;
+
+/**
+ * Generates the Irradiance map for PBR. This job can be launched from a separate
+ * thread.
+ * <p>
+ * This is not used as we use spherical harmonics directly now, but we may need this code again at some point
+ *
+ * @author Nehon
+ */
+public class IrradianceSphericalHarmonicsGenerator extends RunnableWithProgress {
+
+    private TextureCubeMap sourceMap;
+    private LightProbe store;
+    private final Application app;
+
+    /**
+     * Creates an Irradiance map generator. The app is needed to enqueue the
+     * call to the EnvironmentCamera when the generation is done, so that this
+     * process is thread safe.
+     *
+     * @param app      the Application
+     * @param listener
+     */
+    public IrradianceSphericalHarmonicsGenerator(Application app, JobProgressListener<Integer> listener) {
+        super(listener);
+        this.app = app;
+    }
+
+    /**
+     * Fills all the genration parameters
+     *
+     * @param sourceMap the source cube map
+     *                  {@link EnvMapUtils.FixSeamsMethod}
+     * @param store     The cube map to store the result in.
+     */
+    public void setGenerationParam(TextureCubeMap sourceMap, LightProbe store) {
+        this.sourceMap = sourceMap;
+
+        this.store = store;
+        reset();
+    }
+
+    @Override
+    public void run() {
+        app.enqueue(new Callable<Void>() {
+
+            @Override
+            public Void call() throws Exception {
+                listener.start();
+                return null;
+            }
+        });
+        try {
+            Vector3f[] shCoeffs = EnvMapUtils.getSphericalHarmonicsCoefficents(sourceMap);
+            EnvMapUtils.prepareShCoefs(shCoeffs);
+            store.setShCoeffs(shCoeffs);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        app.enqueue(new Callable<Void>() {
+
+            @Override
+            public Void call() throws Exception {
+                listener.done(6);
+                return null;
+            }
+        });
+    }
+
+}

+ 191 - 101
jme3-core/src/main/java/com/jme3/environment/generation/PrefilteredEnvMapFaceGenerator.java

@@ -31,30 +31,23 @@
  */
 package com.jme3.environment.generation;
 
+import com.jme3.app.Application;
 import com.jme3.environment.util.CubeMapWrapper;
 import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.app.Application;
-import com.jme3.math.ColorRGBA;
-import static com.jme3.math.FastMath.abs;
-import static com.jme3.math.FastMath.clamp;
-import static com.jme3.math.FastMath.pow;
-import static com.jme3.math.FastMath.sqrt;
-import com.jme3.math.Vector3f;
-import com.jme3.math.Vector4f;
+import com.jme3.math.*;
 import com.jme3.texture.TextureCubeMap;
-import static com.jme3.environment.util.EnvMapUtils.getHammersleyPoint;
-import static com.jme3.environment.util.EnvMapUtils.getRoughnessFromMip;
-import static com.jme3.environment.util.EnvMapUtils.getSampleFromMip;
-import static com.jme3.environment.util.EnvMapUtils.getVectorFromCubemapFaceTexCoord;
+
 import java.util.concurrent.Callable;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import static com.jme3.environment.util.EnvMapUtils.*;
+import static com.jme3.math.FastMath.abs;
+import static com.jme3.math.FastMath.sqrt;
+
 /**
- *
  * Generates one face of the prefiltered environnement map for PBR. This job can
  * be lauched from a separate thread.
- *
+ * <p>
  * TODO there is a lot of duplicate code here with the EnvMapUtils.
  *
  * @author Nehon
@@ -68,6 +61,7 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
 
     private int targetMapSize;
     private EnvMapUtils.FixSeamsMethod fixSeamsMethod;
+    private EnvMapUtils.GenerationType genType;
     private TextureCubeMap sourceMap;
     private TextureCubeMap store;
     private final Application app;
@@ -85,8 +79,8 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
      * the call to the EnvironmentCamera when the generation is done, so that
      * this process is thread safe.
      *
-     * @param app the Application
-     * @param face the face to generate
+     * @param app      the Application
+     * @param face     the face to generate
      * @param listener
      */
     public PrefilteredEnvMapFaceGenerator(Application app, int face, JobProgressListener<Integer> listener) {
@@ -95,36 +89,35 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
         this.face = face;
     }
 
-    
-    
+
     /**
      * Fills all the genration parameters
      *
-     * @param sourceMap the source cube map
-     * @param targetMapSize the size of the generated map (width or height in
-     * pixel)
+     * @param sourceMap      the source cube map
+     * @param targetMapSize  the size of the generated map (width or height in
+     *                       pixel)
      * @param fixSeamsMethod the method used to fix seams as described here
-     * {@link EnvMapUtils.FixSeamsMethod}
-     *
-     * @param store The cube map to store the result in.
+     *                       {@link EnvMapUtils.FixSeamsMethod}
+     * @param store          The cube map to store the result in.
      */
-    public void setGenerationParam(TextureCubeMap sourceMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
+    public void setGenerationParam(TextureCubeMap sourceMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, EnvMapUtils.GenerationType genType, TextureCubeMap store) {
         this.sourceMap = sourceMap;
         this.targetMapSize = targetMapSize;
         this.fixSeamsMethod = fixSeamsMethod;
         this.store = store;
+        this.genType = genType;
         init();
     }
-    
-    private void init(){
-         Xi.set(0, 0, 0, 0);
-         H.set(0, 0, 0);
-         tmp.set(0, 0, 0);
-         c.set(1, 1, 1, 1);
-         tmp1.set(0, 0, 0);
-         tmp2.set(0, 0, 0);
-         tmp3.set(0, 0, 0);
-         reset();
+
+    private void init() {
+        Xi.set(0, 0, 0, 0);
+        H.set(0, 0, 0);
+        tmp.set(0, 0, 0);
+        c.set(1, 1, 1, 1);
+        tmp1.set(0, 0, 0);
+        tmp2.set(0, 0, 0);
+        tmp3.set(0, 0, 0);
+        reset();
 
     }
 
@@ -157,81 +150,191 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
      * Note that the output cube map is in RGBA8 format.
      *
      * @param sourceEnvMap
-     * @param targetMapSize the size of the irradiance map to generate
+     * @param targetMapSize  the size of the irradiance map to generate
      * @param store
      * @param fixSeamsMethod the method to fix seams
      * @return The irradiance cube map for the given coefficients
      */
     private TextureCubeMap generatePrefilteredEnvMap(TextureCubeMap sourceEnvMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
-        TextureCubeMap pem = store;
-
-        int nbMipMap = (int) (Math.log(targetMapSize) / Math.log(2) - 1);
-
-        setEnd(nbMipMap);
-        
-
-        CubeMapWrapper sourceWrapper = new CubeMapWrapper(sourceEnvMap);
-        CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
-
-        Vector3f texelVect = new Vector3f();
-        Vector3f color = new Vector3f();
-        ColorRGBA outColor = new ColorRGBA();
-        for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) {
-            float roughness = getRoughnessFromMip(mipLevel, nbMipMap);
-            int nbSamples = getSampleFromMip(mipLevel, nbMipMap);
-            int targetMipMapSize = (int) pow(2, nbMipMap + 1 - mipLevel);
-
-            for (int y = 0; y < targetMipMapSize; y++) {
-                for (int x = 0; x < targetMipMapSize; x++) {
-                    color.set(0, 0, 0);
-                    getVectorFromCubemapFaceTexCoord(x, y, targetMipMapSize, face, texelVect, EnvMapUtils.FixSeamsMethod.Wrap);
-                    prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, color);
-                    
-                    outColor.set(Math.max(color.x, 0.0001f), Math.max(color.y,0.0001f), Math.max(color.z, 0.0001f), 1);
-                    log.log(Level.FINE, "coords {0},{1}", new Object[]{x, y});
-                    targetWrapper.setPixel(x, y, face, mipLevel, outColor);
+        try {
+            TextureCubeMap pem = store;
+
+            int nbMipMap = store.getImage().getMipMapSizes().length;
+
+            setEnd(nbMipMap);
+
+            if (!sourceEnvMap.getImage().hasMipmaps() || sourceEnvMap.getImage().getMipMapSizes().length < nbMipMap) {
+                throw new IllegalArgumentException("The input cube map must have at least " + nbMipMap + "mip maps");
+            }
+
+            CubeMapWrapper sourceWrapper = new CubeMapWrapper(sourceEnvMap);
+            CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
 
+            Vector3f texelVect = new Vector3f();
+            Vector3f color = new Vector3f();
+            ColorRGBA outColor = new ColorRGBA();
+            int targetMipMapSize = targetMapSize;
+            for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) {
+                float roughness = getRoughnessFromMip(mipLevel, nbMipMap);
+                int nbSamples = getSampleFromMip(mipLevel, nbMipMap);
+
+                for (int y = 0; y < targetMipMapSize; y++) {
+                    for (int x = 0; x < targetMipMapSize; x++) {
+                        color.set(0, 0, 0);
+                        getVectorFromCubemapFaceTexCoord(x, y, targetMipMapSize, face, texelVect, fixSeamsMethod);
+                        prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, mipLevel, color);
+
+                        outColor.set(Math.max(color.x, 0.0001f), Math.max(color.y, 0.0001f), Math.max(color.z, 0.0001f), 1);
+                        targetWrapper.setPixel(x, y, face, mipLevel, outColor);
+
+                    }
                 }
+                targetMipMapSize /= 2;
+                progress();
             }
-            progress();
-        }
 
-        return pem;
+            return pem;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw e;
+        }
     }
 
-    private Vector3f prefilterEnvMapTexel(CubeMapWrapper envMapReader, float roughness, Vector3f N, int numSamples, Vector3f store) {
+    private Vector3f prefilterEnvMapTexel(CubeMapWrapper envMapReader, float roughness, Vector3f N, int numSamples, int mipLevel, Vector3f store) {
 
         Vector3f prefilteredColor = store;
         float totalWeight = 0.0f;
 
+        int nbRotations = 1;
+        if (genType == GenerationType.HighQuality) {
+            nbRotations = numSamples == 1 ? 1 : 18;
+        }
+
+        float rad = 2f * FastMath.PI / (float) nbRotations;
+        // offset rotation to avoid sampling pattern
+        float gi = (float) (FastMath.abs(N.z + N.x) * 256.0);
+        float offset = rad * (FastMath.cos((gi * 0.5f) % (2f * FastMath.PI)) * 0.5f + 0.5f);
+
         // a = roughness² and a2 = a²
         float a2 = roughness * roughness;
         a2 *= a2;
-        a2 *= 10;
+
+        //Computing tangent frame
+        Vector3f upVector = Vector3f.UNIT_X;
+        if (abs(N.z) < 0.999) {
+            upVector = Vector3f.UNIT_Y;
+        }
+        Vector3f tangentX = tmp1.set(upVector).crossLocal(N).normalizeLocal();
+        Vector3f tangentY = tmp2.set(N).crossLocal(tangentX);
+
+        // https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/
+        // in local space view == normal == 0,0,1
+        Vector3f V = new Vector3f(0, 0, 1);
+
+        Vector3f lWorld = new Vector3f();
         for (int i = 0; i < numSamples; i++) {
             Xi = getHammersleyPoint(i, numSamples, Xi);
-            H = importanceSampleGGX(Xi, a2, N, H);
-
+            H = importanceSampleGGX(Xi, a2, H);
             H.normalizeLocal();
-            tmp.set(H);
-            float NoH = N.dot(tmp);
-
-            Vector3f L = tmp.multLocal(NoH * 2).subtractLocal(N);
-            float NoL = clamp(N.dot(L), 0.0f, 1.0f);
-            if (NoL > 0) {
-                envMapReader.getPixel(L, c);
-                prefilteredColor.setX(prefilteredColor.x + c.r * NoL);
-                prefilteredColor.setY(prefilteredColor.y + c.g * NoL);
-                prefilteredColor.setZ(prefilteredColor.z + c.b * NoL);
-
-                totalWeight += NoL;
+            float VoH = H.z;
+            Vector3f L = H.multLocal(VoH * 2f).subtractLocal(V);
+            float NoL = L.z;
+
+            float computedMipLevel = mipLevel;
+            if (mipLevel != 0) {
+                computedMipLevel = computeMipLevel(roughness, numSamples, this.targetMapSize, VoH);
             }
+
+            toWorld(L, N, tangentX, tangentY, lWorld);
+            totalWeight += samplePixel(envMapReader, lWorld, NoL, computedMipLevel, prefilteredColor);
+
+            for (int j = 1; j < nbRotations; j++) {
+                rotateDirection(offset + j * rad, L, lWorld);
+                L.set(lWorld);
+                toWorld(L, N, tangentX, tangentY, lWorld);
+                totalWeight += samplePixel(envMapReader, lWorld, NoL, computedMipLevel, prefilteredColor);
+            }
+
+        }
+        if (totalWeight > 0) {
+            prefilteredColor.divideLocal(totalWeight);
         }
 
-        return prefilteredColor.divideLocal(totalWeight);
+        return prefilteredColor;
+    }
+
+    private float samplePixel(CubeMapWrapper envMapReader, Vector3f lWorld, float NoL, float computedMipLevel, Vector3f store) {
+
+        if (NoL <= 0) {
+            return 0;
+        }
+        envMapReader.getPixel(lWorld, computedMipLevel, c);
+        store.setX(store.x + c.r * NoL);
+        store.setY(store.y + c.g * NoL);
+        store.setZ(store.z + c.b * NoL);
+
+        return NoL;
+    }
+
+    private void toWorld(Vector3f L, Vector3f N, Vector3f tangentX, Vector3f tangentY, Vector3f store) {
+        store.set(tangentX).multLocal(L.x);
+        tmp.set(tangentY).multLocal(L.y);
+        store.addLocal(tmp);
+        tmp.set(N).multLocal(L.z);
+        store.addLocal(tmp);
     }
 
-    public Vector3f importanceSampleGGX(Vector4f xi, float a2, Vector3f normal, Vector3f store) {
+    private float computeMipLevel(float roughness, int numSamples, float size, float voH) {
+        // H[2] is NoH in local space
+        // adds 1.e-5 to avoid ggx / 0.0
+        float NoH = voH + 1E-5f;
+
+        // Probability Distribution Function
+        float Pdf = ggx(NoH, roughness) * NoH / (4.0f * voH);
+
+        // Solid angle represented by this sample
+        float omegaS = 1.0f / (numSamples * Pdf);
+
+        // Solid angle covered by 1 pixel with 6 faces that are EnvMapSize X EnvMapSize
+        float omegaP = 4.0f * FastMath.PI / (6.0f * size * size);
+
+        // Original paper suggest biasing the mip to improve the results
+        float mipBias = 1.0f; // I tested that the result is better with bias 1
+        double maxLod = Math.log(size) / Math.log(2f);
+        double log2 = Math.log(omegaS / omegaP) / Math.log(2);
+        return Math.min(Math.max(0.5f * (float) log2 + mipBias, 0.0f), (float) maxLod);
+    }
+
+
+    private float ggx(float NoH, float alpha) {
+        // use GGX / Trowbridge-Reitz, same as Disney and Unreal 4
+        // cf http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p3
+        float tmp = alpha / (NoH * NoH * (alpha * alpha - 1.0f) + 1.0f);
+        return tmp * tmp * (1f / FastMath.PI);
+    }
+
+    private Vector3f rotateDirection(float angle, Vector3f l, Vector3f store) {
+        float s, c, t;
+
+        s = FastMath.sin(angle);
+        c = FastMath.cos(angle);
+        t = 1.f - c;
+
+        store.x = l.x * c + l.y * s;
+        store.y = -l.x * s + l.y * c;
+        store.z = l.z * (t + c);
+        return store;
+    }
+
+    /**
+     * Computes GGX half vector in local space
+     *
+     * @param xi
+     * @param a2
+     * @param store
+     * @return
+     */
+    public Vector3f importanceSampleGGX(Vector4f xi, float a2, Vector3f store) {
         if (store == null) {
             store = new Vector3f();
         }
@@ -242,22 +345,9 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
         float sinThetaCosPhi = sinTheta * xi.z;//xi.z is cos(phi)
         float sinThetaSinPhi = sinTheta * xi.w;//xi.w is sin(phi)
 
-        Vector3f upVector = Vector3f.UNIT_X;
-
-        if (abs(normal.z) < 0.999) {
-            upVector = Vector3f.UNIT_Y;
-        }
-
-        Vector3f tangentX = tmp1.set(upVector).crossLocal(normal).normalizeLocal();
-        Vector3f tangentY = tmp2.set(normal).crossLocal(tangentX);
-
-        // Tangent to world space
-        tangentX.multLocal(sinThetaCosPhi);
-        tangentY.multLocal(sinThetaSinPhi);
-        tmp3.set(normal).multLocal(cosTheta);
-
-        // Tangent to world space
-        store.set(tangentX).addLocal(tangentY).addLocal(tmp3);
+        store.x = sinThetaCosPhi;
+        store.y = sinThetaSinPhi;
+        store.z = cosTheta;
 
         return store;
     }

+ 25 - 9
jme3-core/src/main/java/com/jme3/environment/util/CubeMapWrapper.java

@@ -31,17 +31,15 @@
  */
 package com.jme3.environment.util;
 
-import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.math.ColorRGBA;
-import static com.jme3.math.FastMath.pow;
-import com.jme3.math.Vector2f;
-import com.jme3.math.Vector3f;
+import com.jme3.math.*;
 import com.jme3.texture.Image;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.texture.image.DefaultImageRaster;
 import com.jme3.texture.image.MipMapImageRaster;
 import com.jme3.util.BufferUtils;
 
+import static com.jme3.math.FastMath.pow;
+
 /**
  * Wraps a Cube map and allows to read from or write pixels into it.
  * 
@@ -57,6 +55,8 @@ public class CubeMapWrapper {
     private final Vector2f uvs = new Vector2f();
     private final Image image;
 
+    private final ColorRGBA tmpColor = new ColorRGBA();
+
     /**
      * Creates a CubeMapWrapper for the given cube map
      * Note that the cube map must be initialized, and the mipmaps sizes should 
@@ -105,7 +105,7 @@ public class CubeMapWrapper {
      * @param store the color in which to store the pixel color read.
      * @return the color of the pixel read.
      */
-    public ColorRGBA getPixel(Vector3f vector, int mipLevel, ColorRGBA store) {
+    public ColorRGBA getPixel(Vector3f vector, float mipLevel, ColorRGBA store) {
         if (mipMapRaster == null) {
             throw new IllegalArgumentException("This cube map has no mip maps");
         }
@@ -113,10 +113,26 @@ public class CubeMapWrapper {
             store = new ColorRGBA();
         }
 
-        int face = EnvMapUtils.getCubemapFaceTexCoordFromVector(vector, sizes[mipLevel], uvs, EnvMapUtils.FixSeamsMethod.Stretch);
+        int lowerMipLevel = (int) mipLevel;
+        int higherMipLevel = (int) FastMath.ceil(mipLevel);
+        float ratio = mipLevel - lowerMipLevel;
+
+        int face = EnvMapUtils.getCubemapFaceTexCoordFromVector(vector, sizes[lowerMipLevel], uvs, EnvMapUtils.FixSeamsMethod.Stretch);
         mipMapRaster.setSlice(face);
-        mipMapRaster.setMipLevel(mipLevel);
-        return mipMapRaster.getPixel((int) uvs.x, (int) uvs.y, store);
+        mipMapRaster.setMipLevel(lowerMipLevel);
+        mipMapRaster.getPixel((int) uvs.x, (int) uvs.y, store);
+
+        face = EnvMapUtils.getCubemapFaceTexCoordFromVector(vector, sizes[higherMipLevel], uvs, EnvMapUtils.FixSeamsMethod.Stretch);
+        mipMapRaster.setSlice(face);
+        mipMapRaster.setMipLevel(higherMipLevel);
+        mipMapRaster.getPixel((int) uvs.x, (int) uvs.y, tmpColor);
+
+        store.r = FastMath.interpolateLinear(ratio, store.r, tmpColor.r);
+        store.g = FastMath.interpolateLinear(ratio, store.g, tmpColor.g);
+        store.b = FastMath.interpolateLinear(ratio, store.b, tmpColor.b);
+        store.a = FastMath.interpolateLinear(ratio, store.a, tmpColor.a);
+
+        return store;
     }
 
     /**

+ 57 - 257
jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java

@@ -33,25 +33,19 @@ package com.jme3.environment.util;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.material.Material;
-import com.jme3.math.ColorRGBA;
-import com.jme3.math.Vector3f;
-import com.jme3.math.Vector4f;
+import com.jme3.math.*;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Node;
 import com.jme3.scene.shape.Quad;
-import com.jme3.texture.Image;
-import com.jme3.texture.Texture;
-import com.jme3.texture.Texture2D;
-import com.jme3.texture.TextureCubeMap;
+import com.jme3.texture.*;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.ui.Picture;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.TempVars;
+
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
+
 import static com.jme3.math.FastMath.*;
-import com.jme3.math.Quaternion;
-import com.jme3.math.Vector2f;
-import com.jme3.util.TempVars;
 
 /**
  *
@@ -62,6 +56,12 @@ import com.jme3.util.TempVars;
  */
 public class EnvMapUtils {
 
+
+    private static final float sqrtPi = sqrt(PI);
+    private static final float sqrt3Pi = sqrt(3f / PI);
+    private static final float sqrt5Pi = sqrt(5f / PI);
+    private static final float sqrt15Pi = sqrt(15f / PI);
+
     public final static int NUM_SH_COEFFICIENT = 9;
     // See Peter-Pike Sloan paper for these coefficients
     //http://www.ppsloan.org/publications/StupidSH36.pdf
@@ -82,7 +82,12 @@ public class EnvMapUtils {
         /**
          * No seams fix
          */
-        None;
+        None
+    }
+
+    public static enum GenerationType {
+        Fast,
+        HighQuality
     }
 
     /**
@@ -114,17 +119,7 @@ public class EnvMapUtils {
         cubeImage.addData(backImg.getData(0));
         cubeImage.addData(frontImg.getData(0));
 
-        if (leftImg.getEfficentData() != null) {
-            // also consilidate efficient data
-            ArrayList<Object> efficientData = new ArrayList<Object>(6);
-            efficientData.add(rightImg.getEfficentData());
-            efficientData.add(leftImg.getEfficentData());
-            efficientData.add(upImg.getEfficentData());
-            efficientData.add(downImg.getEfficentData());
-            efficientData.add(backImg.getEfficentData());
-            efficientData.add(frontImg.getEfficentData());
-            cubeImage.setEfficentData(efficientData);
-        }
+        cubeImage.setMipMapSizes(rightImg.getMipMapSizes());
 
         TextureCubeMap cubeMap = new TextureCubeMap(cubeImage);
         cubeMap.setAnisotropicFilter(0);
@@ -157,12 +152,7 @@ public class EnvMapUtils {
             cubeImage.addData(d.duplicate());
         }
 
-        if (srcImg.getEfficentData() != null) {
-            // also consilidate efficient data
-            ArrayList<Object> efficientData = new ArrayList<Object>(6);
-            efficientData.add(srcImg.getEfficentData());
-            cubeImage.setEfficentData(efficientData);
-        }
+        cubeImage.setMipMapSizes(srcImg.getMipMapSizes());
 
         TextureCubeMap cubeMap = new TextureCubeMap(cubeImage);
         cubeMap.setAnisotropicFilter(sourceMap.getAnisotropicFilter());
@@ -199,7 +189,7 @@ public class EnvMapUtils {
     static float getSolidAngleAndVector(int x, int y, int mapSize, int face, Vector3f store, FixSeamsMethod fixSeamsMethod) {
 
         if (store == null) {
-            throw new IllegalArgumentException("the store parameter ust not be null");
+            throw new IllegalArgumentException("the store parameter must not be null");
         }
 
         /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)]
@@ -254,7 +244,7 @@ public class EnvMapUtils {
         float v;
 
         if (fixSeamsMethod == FixSeamsMethod.Stretch) {
-            /* Code from Nvtt : http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvtt/CubeSurface.cpp		
+            /* Code from Nvtt : https://github.com/castano/nvidia-texture-tools/blob/master/src/nvtt/CubeSurface.cpp#L77
              * transform from [0..res - 1] to [-1 .. 1], match up edges exactly. */
             u = (2.0f * (float) x / ((float) mapSize - 1.0f)) - 1.0f;
             v = (2.0f * (float) y / ((float) mapSize - 1.0f)) - 1.0f;
@@ -274,7 +264,7 @@ public class EnvMapUtils {
         }
 
         //compute vector depending on the face
-        // Code from Nvtt : http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvtt/CubeSurface.cpp	
+        // Code from Nvtt : https://github.com/castano/nvidia-texture-tools/blob/master/src/nvtt/CubeSurface.cpp#L101
         switch (face) {
             case 0:
                 store.set(1f, -v, -u);
@@ -387,62 +377,21 @@ public class EnvMapUtils {
         return face;
     }
 
-    /*
-    public static void main(String... argv) {
-
-//        for (int givenFace = 0; givenFace < 6; givenFace++) {
-//
-//            //int givenFace = 1;
-//            for (int x = 0; x < 128; x++) {
-//                for (int y = 0; y < 128; y++) {
-//                    Vector3f v = EnvMapUtils.getVectorFromCubemapFaceTexCoord(x, y, 128, givenFace, null, FixSeamsMethod.None);
-//                    Vector2f uvs = new Vector2f();
-//                    int face = EnvMapUtils.getCubemapFaceTexCoordFromVector(v, 128, uvs, FixSeamsMethod.None);
-//
-//                    if ((int) uvs.x != x || (int) uvs.y != y) {
-//                        System.err.println("error " + uvs + " should be " + x + "," + y + " vect was " + v);
-//                    }
-//                    if (givenFace != face) {
-//                        System.err.println("error face: " + face + " should be " + givenFace);
-//                    }
-//                }
-//            }
-//        }
-//        System.err.println("done ");
-        int total = 0;
-        for (int i = 0; i < 6; i++) {
-            int size = (int) pow(2, 7 - i);
-            int samples = EnvMapUtils.getSampleFromMip(i, 6);
-            int iterations = (samples * size * size);
-            total += iterations;
-            float roughness = EnvMapUtils.getRoughnessFromMip(i, 6);
-            System.err.println("roughness " + i + " : " + roughness + " , map : " + size + " , samples : " + samples + " , iterations : " + iterations);
-            System.err.println("reverse " + EnvMapUtils.getMipFromRoughness(roughness, 6));
-
-        }
-        System.err.println("total " + total);
-        System.err.println(128 * 128 * 1024);
-        System.err.println("test " + EnvMapUtils.getMipFromRoughness(0.9999f, 6));
-        System.err.println("nb mip = " + (Math.log(128) / Math.log(2) - 1));
-
-    }*/
-
-    public static int getSampleFromMip(int mipLevel, int miptot) {        
-        return mipLevel==0?1:Math.min(1 << (miptot - 1 + (mipLevel) * 2 ), 8192);
+    public static int getSampleFromMip(int mipLevel, int miptot) {
+        return mipLevel == 0 ? 1 : Math.min(1 << (miptot - 1 + (mipLevel) * 2), 8192);
     }
 
-    public static float getRoughnessFromMip(int miplevel, int miptot) {
-        float mipScale = 1.0f;
-        float mipOffset = -0.3f;
 
-        return pow(2, (miplevel - (miptot - 1) + mipOffset) / mipScale);
+    //see lagarde's paper https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
+    //linear roughness
+    public static float getRoughnessFromMip(int miplevel, int miptot) {
+        float step = 1f / ((float) miptot - 1);
+        step *= miplevel;
+        return step * step;
     }
 
     public static float getMipFromRoughness(float roughness, int miptot) {
-        float mipScale = 1.0f;
-        float Lod = (float) (Math.log(roughness) / Math.log(2)) * mipScale + miptot - 1.0f;       
-
-        return (float) Math.max(0.0, Lod);
+        return FastMath.sqrt(roughness) * (miptot - 1);
     }
 
     /**
@@ -482,7 +431,7 @@ public class EnvMapUtils {
         float weight;
 
         if (cubeMap.getImage().getData(0) == null) {
-            throw new IllegalStateException("The cube map must contain Efficient data, if you rendered the cube map on the GPU plase use renderer.readFrameBuffer, to create a CPU image");
+            throw new IllegalStateException("The cube map must contain Efficient data, if you rendered the cube map on the GPU please use renderer.readFrameBuffer, to create a CPU image");
         }
 
         int width = cubeMap.getImage().getWidth();
@@ -539,12 +488,6 @@ public class EnvMapUtils {
         float yV = texelVect.y;
         float zV = texelVect.z;
 
-        float pi = PI;
-        float sqrtPi = sqrt(pi);
-        float sqrt3Pi = sqrt(3f / pi);
-        float sqrt5Pi = sqrt(5f / pi);
-        float sqrt15Pi = sqrt(15f / pi);
-
         float x2 = xV * xV;
         float y2 = yV * yV;
         float z2 = zV * zV;
@@ -558,140 +501,31 @@ public class EnvMapUtils {
         shDir[6] = (sqrt5Pi * (-1f + 3f * z2)) / 4f;
         shDir[7] = -(sqrt15Pi * xV * zV) / 2f;
         shDir[8] = sqrt15Pi * (x2 - y2) / 4f;
-        
-//        shDir[0]  = (1f/(2.f*sqrtPi));
-//
-//	shDir[1]  = -(sqrt(3f/pi)*yV)/2.f;
-//	shDir[2]  = (sqrt(3/pi)*zV)/2.f;
-//	shDir[3]  = -(sqrt(3/pi)*xV)/2.f;
-//
-//	shDir[4]  = (sqrt(15f/pi)*xV*yV)/2.f;
-//	shDir[5]  = -(sqrt(15f/pi)*yV*zV)/2.f;
-//	shDir[6]  = (sqrt(5f/pi)*(-1 + 3f*z2))/4.f;
-//	shDir[7]  = -(sqrt(15f/pi)*xV*zV)/2.f;
-//	shDir[8]  = sqrt(15f/pi)*(x2 - y2)/4.f;
-
-
     }
 
-    /**
-     * {@link EnvMapUtils#generateIrradianceMap(com.jme3.math.Vector3f[], com.jme3.texture.TextureCubeMap, int, com.jme3.utils.EnvMapUtils.FixSeamsMethod)
-     * }
-     *
-     * @param shCoeffs the spherical harmonics coefficients to use
-     * @param targetMapSize the size of the target map
-     * @return the irradiance map.
-     */
-    public static TextureCubeMap generateIrradianceMap(Vector3f[] shCoeffs, int targetMapSize) {
-        return generateIrradianceMap(shCoeffs, targetMapSize, FixSeamsMethod.Wrap, null);
+    public static void prepareShCoefs(Vector3f[] shCoefs) {
+
+        float coef0 = (1f / (2f * sqrtPi));
+        float coef1 = -sqrt3Pi / 2f;
+        float coef2 = -coef1;
+        float coef3 = coef1;
+        float coef4 = sqrt15Pi / 2f;
+        float coef5 = -coef4;
+        float coef6 = sqrt5Pi / 4f;
+        float coef7 = coef5;
+        float coef8 = sqrt15Pi / 4f;
+
+        shCoefs[0].multLocal(coef0).multLocal(shBandFactor[0]);
+        shCoefs[1].multLocal(coef1).multLocal(shBandFactor[1]);
+        shCoefs[2].multLocal(coef2).multLocal(shBandFactor[2]);
+        shCoefs[3].multLocal(coef3).multLocal(shBandFactor[3]);
+        shCoefs[4].multLocal(coef4).multLocal(shBandFactor[4]);
+        shCoefs[5].multLocal(coef5).multLocal(shBandFactor[5]);
+        shCoefs[6].multLocal(coef6).multLocal(shBandFactor[6]);
+        shCoefs[7].multLocal(coef7).multLocal(shBandFactor[7]);
+        shCoefs[8].multLocal(coef8).multLocal(shBandFactor[8]);
     }
 
-    /**
-     * Generates the Irradiance map (used for image based difuse lighting) from
-     * Spherical Harmonics coefficients previously computed with
-     * {@link EnvMapUtils#getSphericalHarmonicsCoefficents(com.jme3.texture.TextureCubeMap)}
-     * Note that the output cube map is in RGBA8 format.
-     *
-     * @param shCoeffs the SH coeffs
-     * @param targetMapSize the size of the irradiance map to generate
-     * @param fixSeamsMethod the method to fix seams
-     * @param store
-     * @return The irradiance cube map for the given coefficients
-     */
-    public static TextureCubeMap generateIrradianceMap(Vector3f[] shCoeffs, int targetMapSize, FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
-        TextureCubeMap irrCubeMap = store;
-        if (irrCubeMap == null) {
-            irrCubeMap = new TextureCubeMap(targetMapSize, targetMapSize, Image.Format.RGB16F);
-            irrCubeMap.setMagFilter(Texture.MagFilter.Bilinear);
-            irrCubeMap.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
-            irrCubeMap.getImage().setColorSpace(ColorSpace.Linear);
-        }
-
-        for (int i = 0; i < 6; i++) {
-            ByteBuffer buf = BufferUtils.createByteBuffer(targetMapSize * targetMapSize * irrCubeMap.getImage().getFormat().getBitsPerPixel()/8);
-            irrCubeMap.getImage().setData(i, buf);
-        }
-
-        Vector3f texelVect = new Vector3f();
-        ColorRGBA color = new ColorRGBA(ColorRGBA.Black);
-        float[] shDir = new float[9];
-        CubeMapWrapper envMapWriter = new CubeMapWrapper(irrCubeMap);
-        for (int face = 0; face < 6; face++) {
-
-            for (int y = 0; y < targetMapSize; y++) {
-                for (int x = 0; x < targetMapSize; x++) {
-                    getVectorFromCubemapFaceTexCoord(x, y, targetMapSize, face, texelVect, fixSeamsMethod);
-                    evalShBasis(texelVect, shDir);
-                    color.set(0, 0, 0, 0);
-                    for (int i = 0; i < NUM_SH_COEFFICIENT; i++) {
-                        color.set(color.r + shCoeffs[i].x * shDir[i] * shBandFactor[i],
-                                color.g + shCoeffs[i].y * shDir[i] * shBandFactor[i],
-                                color.b + shCoeffs[i].z * shDir[i] * shBandFactor[i],
-                                1.0f);
-                    }
-                    
-                    //clamping the color because very low value close to zero produce artifacts
-                    color.r = Math.max(0.0001f, color.r);
-                    color.g = Math.max(0.0001f, color.g);
-                    color.b = Math.max(0.0001f, color.b);
-                    envMapWriter.setPixel(x, y, face, color);
-                }
-            }
-        }
-        return irrCubeMap;
-    }
-
-    /**
-     * Generates the prefiltered env map (used for image based specular
-     * lighting) With the GGX/Shlick brdf
-     * {@link EnvMapUtils#getSphericalHarmonicsCoefficents(com.jme3.texture.TextureCubeMap)}
-     * Note that the output cube map is in RGBA8 format.
-     *
-     * @param sourceEnvMap
-     * @param targetMapSize the size of the irradiance map to generate
-     * @param store
-     * @param fixSeamsMethod the method to fix seams
-     * @return The irradiance cube map for the given coefficients
-     */
-    public static TextureCubeMap generatePrefilteredEnvMap(TextureCubeMap sourceEnvMap, int targetMapSize, FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
-        TextureCubeMap pem = store;
-        if (pem == null) {
-            pem = new TextureCubeMap(targetMapSize, targetMapSize, Image.Format.RGB16F);
-            pem.setMagFilter(Texture.MagFilter.Bilinear);
-            pem.setMinFilter(Texture.MinFilter.Trilinear);
-            pem.getImage().setColorSpace(ColorSpace.Linear);
-        }
-
-        int nbMipMap = (int) (Math.log(targetMapSize) / Math.log(2) - 1);
-
-        CubeMapWrapper sourceWrapper = new CubeMapWrapper(sourceEnvMap);
-        CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
-        targetWrapper.initMipMaps(nbMipMap);
-
-        Vector3f texelVect = new Vector3f();
-        Vector3f color = new Vector3f();
-        ColorRGBA outColor = new ColorRGBA();
-        for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) {
-            System.err.println("mip level " + mipLevel);
-            float roughness = getRoughnessFromMip(mipLevel, nbMipMap);
-            int nbSamples = getSampleFromMip(mipLevel, nbMipMap);
-            int targetMipMapSize = (int) pow(2, nbMipMap + 1 - mipLevel);
-            for (int face = 0; face < 6; face++) {
-                System.err.println("face " + face);
-                for (int y = 0; y < targetMipMapSize; y++) {
-                    for (int x = 0; x < targetMipMapSize; x++) {
-                        color.set(0, 0, 0);
-                        getVectorFromCubemapFaceTexCoord(x, y, targetMipMapSize, face, texelVect, FixSeamsMethod.Wrap);
-                        prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, color);
-                        outColor.set(color.x, color.y, color.z, 1.0f);
-                        // System.err.println("coords " + x + "," + y);
-                        targetWrapper.setPixel(x, y, face, mipLevel, outColor);
-                    }
-                }
-            }
-        }
-        return pem;
-    }
 
     public static Vector4f getHammersleyPoint(int i, final int nbrSample, Vector4f store) {
         if (store == null) {
@@ -719,43 +553,6 @@ public class EnvMapUtils {
         return store;
     }
 
-    private static Vector3f prefilterEnvMapTexel(CubeMapWrapper envMapReader, float roughness, Vector3f N, int numSamples, Vector3f store) {        
-
-        Vector3f prefilteredColor = store;
-        float totalWeight = 0.0f;
-
-        TempVars vars = TempVars.get();
-        Vector4f Xi = vars.vect4f1;
-        Vector3f H = vars.vect1;
-        Vector3f tmp = vars.vect2;
-        ColorRGBA c = vars.color;
-        // a = roughness² and a2 = a²
-        float a2 = roughness * roughness;
-        a2 *= a2;
-        a2 *= 10;
-        for (int i = 0; i < numSamples; i++) {
-            Xi = getHammersleyPoint(i, numSamples, Xi);            
-            H = importanceSampleGGX(Xi, a2, N, H, vars);
-
-            H.normalizeLocal();
-            tmp.set(H);
-            float NoH = N.dot(tmp);
-
-            Vector3f L = tmp.multLocal(NoH * 2).subtractLocal(N);
-            float NoL = clamp(N.dot(L), 0.0f, 1.0f);
-            if (NoL > 0) {
-                envMapReader.getPixel(L, c);
-                prefilteredColor.setX(prefilteredColor.x + c.r * NoL);
-                prefilteredColor.setY(prefilteredColor.y + c.g * NoL);
-                prefilteredColor.setZ(prefilteredColor.z + c.b * NoL);
-
-                totalWeight += NoL;
-            }
-        }
-        vars.release();
-        return prefilteredColor.divideLocal(totalWeight);
-    }
-
     public static Vector3f importanceSampleGGX(Vector4f xi, float a2, Vector3f normal, Vector3f store, TempVars vars) {
         if (store == null) {
             store = new Vector3f();
@@ -939,9 +736,12 @@ public class EnvMapUtils {
         pem.setMagFilter(Texture.MagFilter.Bilinear);
         pem.setMinFilter(Texture.MinFilter.Trilinear);
         pem.getImage().setColorSpace(ColorSpace.Linear);
-        int nbMipMap = (int) (Math.log(size) / Math.log(2) - 1);
+        int nbMipMap = Math.min(6, (int) (Math.log(size) / Math.log(2)));
         CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
         targetWrapper.initMipMaps(nbMipMap);
         return pem;
     }
 }
+
+
+

+ 1 - 39
jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java

@@ -61,26 +61,10 @@ public class LightsDebugState extends BaseAppState {
     private Geometry debugGeom;
     private Geometry debugBounds;
     private Material debugMaterial;
-    private DebugMode debugMode = DebugMode.PrefilteredEnvMap;
     private float probeScale = 1.0f;
     private Spatial scene = null;
     private final List<LightProbe> probes = new ArrayList<LightProbe>();
 
-    /**
-     * Debug mode for light probes
-     */
-    public enum DebugMode {
-
-        /**
-         * Displays the prefiltered env maps on the debug sphere
-         */
-        PrefilteredEnvMap,
-        /**
-         * displays the Irradiance map on the debug sphere
-         */
-        IrradianceMap
-    }
-
     @Override
     protected void initialize(Application app) {
         debugNode = new Node("Environment debug Node");
@@ -114,11 +98,7 @@ public class LightsDebugState extends BaseAppState {
                     Material m = probeGeom.getMaterial();
                     probeGeom.setLocalScale(probeScale);
                     if (probe.isReady()) {
-                        if (debugMode == DebugMode.IrradianceMap) {
-                            m.setTexture("CubeMap", probe.getIrradianceMap());
-                        } else {
-                            m.setTexture("CubeMap", probe.getPrefilteredEnvMap());
-                        }
+                        m.setTexture("CubeMap", probe.getPrefilteredEnvMap());
                     }
                     n.setLocalTranslation(probe.getPosition());
                     n.getChild(1).setLocalScale(((BoundingSphere) probe.getBounds()).getRadius());
@@ -161,24 +141,6 @@ public class LightsDebugState extends BaseAppState {
         rm.renderScene(debugNode, getApplication().getViewPort());
     }
 
-    /**
-     * 
-     * @see DebugMode
-     * @return the debug mode
-     */
-    public DebugMode getDebugMode() {
-        return debugMode;
-    }
-
-    /**
-     * sets the debug mode
-     * @see DebugMode
-     * @param debugMode the debug mode
-     */
-    public void setDebugMode(DebugMode debugMode) {
-        this.debugMode = debugMode;
-
-    }
 
     /**
      * returns the scale of the probe's debug sphere

+ 11 - 5
jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java

@@ -34,6 +34,7 @@ package com.jme3.export;
 import com.jme3.animation.Animation;
 import com.jme3.effect.shapes.*;
 import com.jme3.material.MatParamTexture;
+
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
@@ -187,16 +188,21 @@ public class SavableClassUtil {
         if (loaders == null) {
             return fromName(className);
         }
-        
+
         String newClassName = remapClass(className);
-        synchronized(loaders) {
-            for (ClassLoader classLoader : loaders){
+        synchronized (loaders) {
+            for (ClassLoader classLoader : loaders) {
+                final Class<?> loadedClass;
                 try {
-                    return (Savable) classLoader.loadClass(newClassName).newInstance();
+                    loadedClass = classLoader.loadClass(newClassName);
+                } catch (final ClassNotFoundException e) {
+                    continue;
+                }
+                try {
+                    return (Savable) loadedClass.newInstance();
                 } catch (InstantiationException e) {
                 } catch (IllegalAccessException e) {
                 }
-
             }
         }
 

+ 167 - 89
jme3-core/src/main/java/com/jme3/input/FlyByCamera.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2017 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,7 +32,11 @@
 package com.jme3.input;
 
 import com.jme3.collision.MotionAllowedListener;
-import com.jme3.input.controls.*;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.MouseAxisTrigger;
+import com.jme3.input.controls.MouseButtonTrigger;
 import com.jme3.math.FastMath;
 import com.jme3.math.Matrix3f;
 import com.jme3.math.Quaternion;
@@ -40,12 +44,13 @@ import com.jme3.math.Vector3f;
 import com.jme3.renderer.Camera;
 
 /**
- * A first person view camera controller.
- * After creation, you must register the camera controller with the
- * dispatcher using #registerWithDispatcher().
+ * A first-person camera controller.
+ *
+ * After creation, you (or FlyCamAppState) must register the controller using
+ * {@link #registerWithInput(com.jme3.input.InputManager)}.
  *
  * Controls:
- *  - Move the mouse to rotate the camera
+ *  - Move (or, in drag-to-rotate mode, drag) the mouse to rotate the camera
  *  - Mouse wheel for zooming in or out
  *  - WASD keys for moving forward/backward and strafing
  *  - QZ keys raise or lower the camera
@@ -53,41 +58,62 @@ import com.jme3.renderer.Camera;
 public class FlyByCamera implements AnalogListener, ActionListener {
 
     private static String[] mappings = new String[]{
-            CameraInput.FLYCAM_LEFT,
-            CameraInput.FLYCAM_RIGHT,
-            CameraInput.FLYCAM_UP,
-            CameraInput.FLYCAM_DOWN,
-
-            CameraInput.FLYCAM_STRAFELEFT,
-            CameraInput.FLYCAM_STRAFERIGHT,
-            CameraInput.FLYCAM_FORWARD,
-            CameraInput.FLYCAM_BACKWARD,
-
-            CameraInput.FLYCAM_ZOOMIN,
-            CameraInput.FLYCAM_ZOOMOUT,
-            CameraInput.FLYCAM_ROTATEDRAG,
-
-            CameraInput.FLYCAM_RISE,
-            CameraInput.FLYCAM_LOWER,
-            
-            CameraInput.FLYCAM_INVERTY
-        };
+        CameraInput.FLYCAM_LEFT,
+        CameraInput.FLYCAM_RIGHT,
+        CameraInput.FLYCAM_UP,
+        CameraInput.FLYCAM_DOWN,
+
+        CameraInput.FLYCAM_STRAFELEFT,
+        CameraInput.FLYCAM_STRAFERIGHT,
+        CameraInput.FLYCAM_FORWARD,
+        CameraInput.FLYCAM_BACKWARD,
+
+        CameraInput.FLYCAM_ZOOMIN,
+        CameraInput.FLYCAM_ZOOMOUT,
+        CameraInput.FLYCAM_ROTATEDRAG,
 
+        CameraInput.FLYCAM_RISE,
+        CameraInput.FLYCAM_LOWER,
+
+        CameraInput.FLYCAM_INVERTY
+    };
+    /**
+     * camera controlled by this controller (not null)
+     */
     protected Camera cam;
+    /**
+     * normalized "up" direction (a unit vector)
+     */
     protected Vector3f initialUpVec;
+    /**
+     * rotation-rate multiplier (1=default)
+     */
     protected float rotationSpeed = 1f;
+    /**
+     * translation speed (in world units per second)
+     */
     protected float moveSpeed = 3f;
+    /**
+     * zoom-rate multiplier (1=default)
+     */
     protected float zoomSpeed = 1f;
     protected MotionAllowedListener motionAllowed = null;
+    /**
+     * enable flag for controller (false&rarr;ignoring input)
+     */
     protected boolean enabled = true;
+    /**
+     * drag-to-rotate mode flag
+     */
     protected boolean dragToRotate = false;
     protected boolean canRotate = false;
     protected boolean invertY = false;
     protected InputManager inputManager;
-    
+
     /**
-     * Creates a new FlyByCamera to control the given Camera object.
-     * @param cam
+     * Creates a new FlyByCamera to control the specified camera.
+     *
+     * @param cam camera to be controlled (not null)
      */
     public FlyByCamera(Camera cam){
         this.cam = cam;
@@ -96,10 +122,11 @@ public class FlyByCamera implements AnalogListener, ActionListener {
 
     /**
      * Sets the up vector that should be used for the camera.
+     *
      * @param upVec
      */
     public void setUpVector(Vector3f upVec) {
-       initialUpVec.set(upVec);
+        initialUpVec.set(upVec);
     }
 
     public void setMotionAllowedListener(MotionAllowedListener listener){
@@ -107,56 +134,68 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     }
 
     /**
-     * Sets the move speed. The speed is given in world units per second.
-     * @param moveSpeed
+     * Set the translation speed.
+     *
+     * @param moveSpeed new speed (in world units per second)
      */
     public void setMoveSpeed(float moveSpeed){
         this.moveSpeed = moveSpeed;
     }
-    
+
     /**
-     * Gets the move speed. The speed is given in world units per second.
-     * @return moveSpeed
+     * Read the translation speed.
+     *
+     * @return current speed (in world units per second)
      */
     public float getMoveSpeed(){
         return moveSpeed;
     }
 
     /**
-     * Sets the rotation speed.
-     * @param rotationSpeed
+     * Set the rotation-rate multiplier. The bigger the multiplier, the more
+     * rotation for a given movement of the mouse.
+     *
+     * @param rotationSpeed new rate multiplier (1=default)
      */
     public void setRotationSpeed(float rotationSpeed){
         this.rotationSpeed = rotationSpeed;
     }
-    
+
     /**
-     * Gets the move speed. The speed is given in world units per second.
-     * @return rotationSpeed
+     * Read the rotation-rate multiplier. The bigger the multiplier, the more
+     * rotation for a given movement of the mouse.
+     *
+     * @return current rate multiplier (1=default)
      */
     public float getRotationSpeed(){
         return rotationSpeed;
     }
-    
+
     /**
-     * Sets the zoom speed.
-     * @param zoomSpeed 
+     * Set the zoom-rate multiplier. The bigger the multiplier, the more zoom
+     * for a given movement of the mouse wheel.
+     *
+     * @param zoomSpeed new rate multiplier (1=default)
      */
     public void setZoomSpeed(float zoomSpeed) {
         this.zoomSpeed = zoomSpeed;
     }
-    
+
     /**
-     * Gets the zoom speed.  The speed is a multiplier to increase/decrease
-     * the zoom rate.
-     * @return zoomSpeed
+     * Read the zoom-rate multiplier. The bigger the multiplier, the more zoom
+     * for a given movement of the mouse wheel.
+     *
+     * @return current rate multiplier (1=default)
      */
     public float getZoomSpeed() {
         return zoomSpeed;
     }
 
     /**
-     * @param enable If false, the camera will ignore input.
+     * Enable or disable this controller. When disabled, the controller ignored
+     * input.
+     *
+     * @param enable true to enable, false to disable
      */
     public void setEnabled(boolean enable){
         if (enabled && !enable){
@@ -168,32 +207,36 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     }
 
     /**
-     * @return If enabled
-     * @see FlyByCamera#setEnabled(boolean)
+     * Test whether this controller is enabled.
+     *
+     * @return true if enabled, otherwise false
+     * @see #setEnabled(boolean)
      */
     public boolean isEnabled(){
         return enabled;
     }
 
     /**
+     * Test whether drag-to-rotate mode is enabled.
+     *
      * @return If drag to rotate feature is enabled.
      *
-     * @see FlyByCamera#setDragToRotate(boolean) 
+     * @see #setDragToRotate(boolean)
      */
     public boolean isDragToRotate() {
         return dragToRotate;
     }
 
     /**
-     * Set if drag to rotate mode is enabled.
-     * 
-     * When true, the user must hold the mouse button
-     * and drag over the screen to rotate the camera, and the cursor is
-     * visible until dragged. Otherwise, the cursor is invisible at all times
-     * and holding the mouse button is not needed to rotate the camera.
-     * This feature is disabled by default.
-     * 
-     * @param dragToRotate True if drag to rotate mode is enabled.
+     * Enable or disable drag-to-rotate mode.
+     *
+     * When drag-to-rotate mode is enabled, the user must hold the mouse button
+     * and drag over the screen to rotate the camera, and the cursor is visible
+     * until dragged. When drag-to-rotate mode is disabled, the cursor is
+     * invisible at all times and holding the mouse button is not needed to
+     * rotate the camera. This mode is disabled by default.
+     *
+     * @param dragToRotate true to enable, false to disable
      */
     public void setDragToRotate(boolean dragToRotate) {
         this.dragToRotate = dragToRotate;
@@ -203,25 +246,26 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     }
 
     /**
-     * Registers the FlyByCamera to receive input events from the provided
-     * Dispatcher.
+     * Register this controller to receive input events from the specified input
+     * manager.
+     *
      * @param inputManager
      */
     public void registerWithInput(InputManager inputManager){
         this.inputManager = inputManager;
-        
+
         // both mouse and button - rotation of cam
         inputManager.addMapping(CameraInput.FLYCAM_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true),
-                                               new KeyTrigger(KeyInput.KEY_LEFT));
+                new KeyTrigger(KeyInput.KEY_LEFT));
 
         inputManager.addMapping(CameraInput.FLYCAM_RIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, false),
-                                                new KeyTrigger(KeyInput.KEY_RIGHT));
+                new KeyTrigger(KeyInput.KEY_RIGHT));
 
         inputManager.addMapping(CameraInput.FLYCAM_UP, new MouseAxisTrigger(MouseInput.AXIS_Y, false),
-                                             new KeyTrigger(KeyInput.KEY_UP));
+                new KeyTrigger(KeyInput.KEY_UP));
 
         inputManager.addMapping(CameraInput.FLYCAM_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, true),
-                                               new KeyTrigger(KeyInput.KEY_DOWN));
+                new KeyTrigger(KeyInput.KEY_DOWN));
 
         // mouse only - zoom in/out with wheel, and rotate drag
         inputManager.addMapping(CameraInput.FLYCAM_ZOOMIN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
@@ -248,43 +292,42 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     }
 
     protected void mapJoystick( Joystick joystick ) {
-        
+
         // Map it differently if there are Z axis
         if( joystick.getAxis( JoystickAxis.Z_ROTATION ) != null && joystick.getAxis( JoystickAxis.Z_AXIS ) != null ) {
- 
+
             // Make the left stick move
             joystick.getXAxis().assignAxis( CameraInput.FLYCAM_STRAFERIGHT, CameraInput.FLYCAM_STRAFELEFT );
             joystick.getYAxis().assignAxis( CameraInput.FLYCAM_BACKWARD, CameraInput.FLYCAM_FORWARD );
-            
-            // And the right stick control the camera                       
+
+            // And the right stick control the camera
             joystick.getAxis( JoystickAxis.Z_ROTATION ).assignAxis( CameraInput.FLYCAM_DOWN, CameraInput.FLYCAM_UP );
             joystick.getAxis( JoystickAxis.Z_AXIS ).assignAxis(  CameraInput.FLYCAM_RIGHT, CameraInput.FLYCAM_LEFT );
- 
-            // And let the dpad be up and down           
+
+            // And let the dpad be up and down
             joystick.getPovYAxis().assignAxis(CameraInput.FLYCAM_RISE, CameraInput.FLYCAM_LOWER);
- 
-            if( joystick.getButton( "Button 8" ) != null ) { 
+
+            if( joystick.getButton( "Button 8" ) != null ) {
                 // Let the stanard select button be the y invert toggle
                 joystick.getButton( "Button 8" ).assignButton( CameraInput.FLYCAM_INVERTY );
-            }                
-            
-        } else {             
+            }
+
+        } else {
             joystick.getPovXAxis().assignAxis(CameraInput.FLYCAM_STRAFERIGHT, CameraInput.FLYCAM_STRAFELEFT);
             joystick.getPovYAxis().assignAxis(CameraInput.FLYCAM_FORWARD, CameraInput.FLYCAM_BACKWARD);
             joystick.getXAxis().assignAxis(CameraInput.FLYCAM_RIGHT, CameraInput.FLYCAM_LEFT);
             joystick.getYAxis().assignAxis(CameraInput.FLYCAM_DOWN, CameraInput.FLYCAM_UP);
-        }                
+        }
     }
 
     /**
-     * Unregisters the FlyByCamera from the event Dispatcher.
+     * Unregister this controller from its input manager.
      */
     public void unregisterInput(){
-    
         if (inputManager == null) {
             return;
         }
-    
+
         for (String s : mappings) {
             if (inputManager.hasMapping(s)) {
                 inputManager.deleteMapping( s );
@@ -296,12 +339,16 @@ public class FlyByCamera implements AnalogListener, ActionListener {
 
         Joystick[] joysticks = inputManager.getJoysticks();
         if (joysticks != null && joysticks.length > 0){
-            Joystick joystick = joysticks[0];
-            
-            // No way to unassing axis
+            // No way to unassign axis
         }
     }
 
+    /**
+     * Rotate the camera by the specified amount around the specified axis.
+     *
+     * @param value rotation amount
+     * @param axis direction of rotation (a unit vector)
+     */
     protected void rotateCamera(float value, Vector3f axis){
         if (dragToRotate){
             if (canRotate){
@@ -329,6 +376,11 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         cam.setAxes(q);
     }
 
+    /**
+     * Zoom the camera by the specified amount.
+     *
+     * @param value zoom amount
+     */
     protected void zoomCamera(float value){
         // derive fovY value
         float h = cam.getFrustumTop();
@@ -338,7 +390,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         float near = cam.getFrustumNear();
 
         float fovY = FastMath.atan(h / near)
-                  / (FastMath.DEG_TO_RAD * .5f);
+                / (FastMath.DEG_TO_RAD * .5f);
         float newFovY = fovY + value * 0.1f * zoomSpeed;
         if (newFovY > 0f) {
             // Don't let the FOV go zero or negative.
@@ -354,6 +406,11 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         cam.setFrustumRight(w);
     }
 
+    /**
+     * Translate the camera upward by the specified amount.
+     *
+     * @param value translation amount
+     */
     protected void riseCamera(float value){
         Vector3f vel = new Vector3f(0, value * moveSpeed, 0);
         Vector3f pos = cam.getLocation().clone();
@@ -366,6 +423,12 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         cam.setLocation(pos);
     }
 
+    /**
+     * Translate the camera left or forward by the specified amount.
+     *
+     * @param value translation amount
+     * @param sideways true&rarr;left, false&rarr;forward
+     */
     protected void moveCamera(float value, boolean sideways){
         Vector3f vel = new Vector3f();
         Vector3f pos = cam.getLocation().clone();
@@ -385,6 +448,14 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         cam.setLocation(pos);
     }
 
+    /**
+     * Callback to notify this controller of an analog input event.
+     *
+     * @param name name of the input event
+     * @param value value of the axis (from 0 to 1)
+     * @param tpf time per frame (in seconds)
+     */
+    @Override
     public void onAnalog(String name, float value, float tpf) {
         if (!enabled)
             return;
@@ -416,6 +487,14 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         }
     }
 
+    /**
+     * Callback to notify this controller of an action input event.
+     *
+     * @param name name of the input event
+     * @param value true if the action is "pressed", false otherwise
+     * @param tpf time per frame (in seconds)
+     */
+    @Override
     public void onAction(String name, boolean value, float tpf) {
         if (!enabled)
             return;
@@ -424,11 +503,10 @@ public class FlyByCamera implements AnalogListener, ActionListener {
             canRotate = value;
             inputManager.setCursorVisible(!value);
         } else if (name.equals(CameraInput.FLYCAM_INVERTY)) {
-            // Toggle on the up.
-            if( !value ) {  
+            // Invert the "up" direction.
+            if( !value ) {
                 invertY = !invertY;
             }
-        }        
+        }
     }
-
 }

+ 43 - 29
jme3-core/src/main/java/com/jme3/light/LightProbe.java

@@ -50,17 +50,19 @@ import com.jme3.scene.Spatial;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.util.TempVars;
 import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * A LightProbe is not exactly a light. It holds environment map information used for Image Based Lighting.
  * This is used for indirect lighting in the Physically Based Rendering pipeline.
  * 
  * A light probe has a position in world space. This is the position from where the Environment Map are rendered.
- * There are two environment maps held by the LightProbe :
- * - The irradiance map (used for indirect diffuse lighting in the PBR pipeline).
+ * There are two environment data structure  held by the LightProbe :
+ * - The irradiance spherical harmonics factors (used for indirect diffuse lighting in the PBR pipeline).
  * - The prefiltered environment map (used for indirect specular lighting and reflection in the PBE pipeline).
- * Note that when instanciating the LightProbe, both those maps are null. 
- * To render them see {@link LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Node)}
+ * Note that when instantiating the LightProbe, both of those structures are null.
+ * To compute them see {@link LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Node)}
  * and {@link EnvironmentCamera}.
  * 
  * The light probe has an area of effect that is a bounding volume centered on its position. (for now only Bounding spheres are supported).
@@ -75,12 +77,15 @@ import java.io.IOException;
  */
 public class LightProbe extends Light implements Savable {
 
-    private TextureCubeMap irradianceMap;
+    private static final Logger logger = Logger.getLogger(LightProbe.class.getName());
+
+    private Vector3f[] shCoeffs;
     private TextureCubeMap prefilteredEnvMap;
     private BoundingVolume bounds = new BoundingSphere(1.0f, Vector3f.ZERO);
     private boolean ready = false;
     private Vector3f position = new Vector3f();
     private Node debugNode;
+    private int nbMipMaps;
 
     /**
      * Empty constructor used for serialization. 
@@ -89,23 +94,6 @@ public class LightProbe extends Light implements Savable {
     public LightProbe() {        
     }
 
-    /**
-     * returns the irradiance map texture of this Light probe.
-     * Note that this Texture may not have image data yet if the LightProbe is not ready
-     * @return the irradiance map 
-     */
-    public TextureCubeMap getIrradianceMap() {
-        return irradianceMap;
-    }
-
-    /**
-     * Sets the irradiance map
-     * @param irradianceMap the irradiance map
-     */
-    public void setIrradianceMap(TextureCubeMap irradianceMap) {
-        this.irradianceMap = irradianceMap;
-    }
-
     /**
      * returns the prefiltered environment map texture of this light probe
      * Note that this Texture may not have image data yet if the LightProbe is not ready
@@ -127,22 +115,35 @@ public class LightProbe extends Light implements Savable {
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
-        oc.write(irradianceMap, "irradianceMap", null);
+        oc.write(shCoeffs, "shCoeffs", null);
         oc.write(prefilteredEnvMap, "prefilteredEnvMap", null);
         oc.write(position, "position", null);
         oc.write(bounds, "bounds", new BoundingSphere(1.0f, Vector3f.ZERO));
         oc.write(ready, "ready", false);
+        oc.write(nbMipMaps, "nbMipMaps", 0);
     }
 
     @Override
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
-        irradianceMap = (TextureCubeMap) ic.readSavable("irradianceMap", null);
+        
         prefilteredEnvMap = (TextureCubeMap) ic.readSavable("prefilteredEnvMap", null);
-        position = (Vector3f) ic.readSavable("position", this);
+        position = (Vector3f) ic.readSavable("position", null);
         bounds = (BoundingVolume) ic.readSavable("bounds", new BoundingSphere(1.0f, Vector3f.ZERO));
+        nbMipMaps = ic.readInt("nbMipMaps", 0);
         ready = ic.readBoolean("ready", false);
+
+        Savable[] coeffs = ic.readSavableArray("shCoeffs", null);
+        if (coeffs == null) {
+            ready = false;
+            logger.log(Level.WARNING, "LightProbe is missing parameters, it should be recomputed. Please use lightProbeFactory.updateProbe()");
+        } else {
+            shCoeffs = new Vector3f[coeffs.length];
+            for (int i = 0; i < coeffs.length; i++) {
+                shCoeffs[i] = (Vector3f) coeffs[i];
+            }
+        }
     }
 
     /**
@@ -194,14 +195,11 @@ public class LightProbe extends Light implements Savable {
      */
     public Node getDebugGui(AssetManager manager) {
         if (!ready) {
-            throw new UnsupportedOperationException("This EnvProbeis not ready yet, try to test isReady()");
+            throw new UnsupportedOperationException("This EnvProbe is not ready yet, try to test isReady()");
         }
         if (debugNode == null) {
             debugNode = new Node("debug gui probe");
             Node debugPfemCm = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(getPrefilteredEnvMap(), manager);
-            Node debugIrrCm = EnvMapUtils.getCubeMapCrossDebugView(getIrradianceMap(), manager);
-
-            debugNode.attachChild(debugIrrCm);
             debugNode.attachChild(debugPfemCm);
             debugPfemCm.setLocalTranslation(520, 0, 0);
         }
@@ -209,6 +207,14 @@ public class LightProbe extends Light implements Savable {
         return debugNode;
     }
 
+    public Vector3f[] getShCoeffs() {
+        return shCoeffs;
+    }
+
+    public void setShCoeffs(Vector3f[] shCoeffs) {
+        this.shCoeffs = shCoeffs;
+    }
+
     /**
      * Returns the position of the LightProbe in world space
      * @return the wolrd space position
@@ -226,6 +232,14 @@ public class LightProbe extends Light implements Savable {
         getBounds().setCenter(position);
     }
 
+    public int getNbMipMaps() {
+        return nbMipMaps;
+    }
+
+    public void setNbMipMaps(int nbMipMaps) {
+        this.nbMipMaps = nbMipMaps;
+    }
+
     @Override
     public boolean intersectsBox(BoundingBox box, TempVars vars) {
         return getBounds().intersectsBoundingBox(box);

+ 11 - 9
jme3-core/src/main/java/com/jme3/material/MatParam.java

@@ -37,6 +37,7 @@ import com.jme3.math.*;
 import com.jme3.shader.VarType;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture.WrapMode;
+
 import java.io.IOException;
 
 /**
@@ -301,20 +302,21 @@ When arrays can be inserted in J3M files
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(type, "varType", null);
         oc.write(name, "name", null);
+
+        if (value == null) {
+            return;
+        }
+
         if (value instanceof Savable) {
-            Savable s = (Savable) value;
-            oc.write(s, "value_savable", null);
+            oc.write((Savable) value, "value_savable", null);
         } else if (value instanceof Float) {
-            Float f = (Float) value;
-            oc.write(f.floatValue(), "value_float", 0f);
+            oc.write((Float) value, "value_float", 0f);
         } else if (value instanceof Integer) {
-            Integer i = (Integer) value;
-            oc.write(i.intValue(), "value_int", 0);
+            oc.write((Integer) value, "value_int", 0);
         } else if (value instanceof Boolean) {
-            Boolean b = (Boolean) value;
-            oc.write(b.booleanValue(), "value_bool", false);
+            oc.write((Boolean) value, "value_bool", false);
         } else if (value.getClass().isArray() && value instanceof Savable[]) {
-            oc.write((Savable[])value, "value_savable_array", null);
+            oc.write((Savable[]) value, "value_savable_array", null);
         }
     }
 

+ 10 - 3
jme3-core/src/main/java/com/jme3/material/ShaderGenerationInfo.java

@@ -190,7 +190,13 @@ public class ShaderGenerationInfo implements Savable, Cloneable {
 
     @Override
     protected ShaderGenerationInfo clone() throws CloneNotSupportedException {
-        ShaderGenerationInfo clone = (ShaderGenerationInfo) super.clone();
+        final ShaderGenerationInfo clone = (ShaderGenerationInfo) super.clone();
+        clone.attributes = new ArrayList<>();
+        clone.vertexUniforms = new ArrayList<>();
+        clone.fragmentUniforms = new ArrayList<>();
+        clone.fragmentGlobals = new ArrayList<>();
+        clone.unusedNodes = new ArrayList<>();
+        clone.varyings = new ArrayList<>();
 
         for (ShaderNodeVariable attribute : attributes) {
             clone.attributes.add(attribute.clone());
@@ -200,8 +206,9 @@ public class ShaderGenerationInfo implements Savable, Cloneable {
             clone.vertexUniforms.add(uniform.clone());
         }
 
-        clone.vertexGlobal = vertexGlobal.clone();
-
+        if (vertexGlobal != null) {
+            clone.vertexGlobal = vertexGlobal.clone();
+        }
 
         for (ShaderNodeVariable varying : varyings) {
             clone.varyings.add(varying.clone());

+ 8 - 14
jme3-core/src/main/java/com/jme3/material/TechniqueDef.java

@@ -188,6 +188,7 @@ public class TechniqueDef implements Savable, Cloneable {
         defineTypes = new ArrayList<VarType>();
         paramToDefineId = new HashMap<String, Integer>();
         definesToShaderMap = new HashMap<DefineList, Shader>();
+        worldBinds = new ArrayList<>();
     }
     
     /**
@@ -513,10 +514,8 @@ public class TechniqueDef implements Savable, Cloneable {
             }
         }
 
-        if (getWorldBindings() != null) {
-           for (UniformBinding binding : getWorldBindings()) {
-               shader.addUniformBinding(binding);
-           }
+        for (final UniformBinding binding : getWorldBindings()) {
+            shader.addUniformBinding(binding);
         }
         
         return shader;
@@ -625,14 +624,10 @@ public class TechniqueDef implements Savable, Cloneable {
      * to the list of world parameters, false otherwise.
      */
     public boolean addWorldParam(String name) {
-        if (worldBinds == null){
-            worldBinds = new ArrayList<UniformBinding>();
-        }
-
         try {
-            worldBinds.add( UniformBinding.valueOf(name) );
+            worldBinds.add(UniformBinding.valueOf(name));
             return true;
-        } catch (IllegalArgumentException ex){
+        } catch (IllegalArgumentException ex) {
             return false;
         }
     }
@@ -801,6 +796,7 @@ public class TechniqueDef implements Savable, Cloneable {
         clone.paramToDefineId.putAll(paramToDefineId);
 
         if (shaderNodes != null) {
+            clone.shaderNodes = new ArrayList<>();
             for (ShaderNode shaderNode : shaderNodes) {
                 clone.shaderNodes.add(shaderNode.clone());
             }
@@ -820,10 +816,8 @@ public class TechniqueDef implements Savable, Cloneable {
             e.printStackTrace();
         }
 
-        if (worldBinds != null) {
-            clone.worldBinds = new ArrayList<>(worldBinds.size());
-            clone.worldBinds.addAll(worldBinds);
-        }
+        clone.worldBinds = new ArrayList<>(worldBinds.size());
+        clone.worldBinds.addAll(worldBinds);
 
         return clone;
     }

+ 6 - 7
jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java

@@ -115,7 +115,9 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
         Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
         Uniform lightProbeData = shader.getUniform("g_LightProbeData");
         lightProbeData.setVector4Length(1);
-        Uniform lightProbeIrrMap = shader.getUniform("g_IrradianceMap");
+
+        //TODO These 2 uniforms should be packed in an array, to ba able to have several probes and blend between them.
+        Uniform shCoeffs = shader.getUniform("g_ShCoeffs");
         Uniform lightProbePemMap = shader.getUniform("g_PrefEnvMap");
 
         lightProbe = null;
@@ -128,16 +130,13 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
             ambientColor.setValue(VarType.Vector4, ambientLightColor);
         }
 
-        //If there is a lightProbe in the list we force it's render on the first pass
+        //If there is a lightProbe in the list we force its render on the first pass
         if(lightProbe != null){
             BoundingSphere s = (BoundingSphere)lightProbe.getBounds();
-            lightProbeData.setVector4InArray(lightProbe.getPosition().x, lightProbe.getPosition().y, lightProbe.getPosition().z, 1f/s.getRadius(), 0);
+            lightProbeData.setVector4InArray(lightProbe.getPosition().x, lightProbe.getPosition().y, lightProbe.getPosition().z, 1f / s.getRadius() + lightProbe.getNbMipMaps(), 0);
+            shCoeffs.setValue(VarType.Vector3Array, lightProbe.getShCoeffs());
             //assigning new texture indexes
-            int irrUnit = lastTexUnit++;
             int pemUnit = lastTexUnit++;
-
-            rm.getRenderer().setTexture(irrUnit, lightProbe.getIrradianceMap());
-            lightProbeIrrMap.setValue(VarType.Int, irrUnit);
             rm.getRenderer().setTexture(pemUnit, lightProbe.getPrefilteredEnvMap());
             lightProbePemMap.setValue(VarType.Int, pemUnit);
         } else {

+ 7 - 10
jme3-core/src/main/java/com/jme3/math/Transform.java

@@ -186,27 +186,24 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
     }
 
     /**
-     * Changes the values of this matrix acording to it's parent.  Very similar to the concept of Node/Spatial transforms.
+     * Changes the values of this matrix according to it's parent.  Very similar to the concept of Node/Spatial transforms.
      * @param parent The parent matrix.
      * @return This matrix, after combining.
      */
     public Transform combineWithParent(Transform parent) {
+        //applying parent scale to local scale
         scale.multLocal(parent.scale);
-//        rot.multLocal(parent.rot);
+        //applying parent rotation to local rotation.
         parent.rot.mult(rot, rot);
-
-        // This here, is evil code
-//        parent
-//            .rot
-//            .multLocal(translation)
-//            .multLocal(parent.scale)
-//            .addLocal(parent.translation);
-
+        //applying parent scale to local translation.
         translation.multLocal(parent.scale);
+        //applying parent rotation to local translation, then applying parent translation to local translation.
+        //Note that parent.rot.multLocal(translation) doesn't modify "parent.rot" but "translation"
         parent
             .rot
             .multLocal(translation)
             .addLocal(parent.translation);
+
         return this;
     }
 

+ 2 - 14
jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java

@@ -45,10 +45,6 @@ public class OpenCLObjectManager {
     private static final Logger LOG = Logger.getLogger(OpenCLObjectManager.class.getName());
     private static final Level LOG_LEVEL1 = Level.FINER;
     private static final Level LOG_LEVEL2 = Level.FINE;
-    /**
-     * Call Runtime.getRuntime().gc() every these frames
-     */
-    private static final int GC_FREQUENCY = 10;
     
     private static final OpenCLObjectManager INSTANCE = new OpenCLObjectManager();
     private OpenCLObjectManager() {}
@@ -59,7 +55,6 @@ public class OpenCLObjectManager {
     
     private ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
     private HashSet<OpenCLObjectRef> activeObjects = new HashSet<OpenCLObjectRef>();
-    private int gcCounter = 0;
     
     private static class OpenCLObjectRef extends PhantomReference<Object> {
         
@@ -80,6 +75,7 @@ public class OpenCLObjectManager {
     private void deleteObject(OpenCLObjectRef ref) {
         LOG.log(LOG_LEVEL1, "deleting OpenCL object by: {0}", ref.releaser);
         ref.releaser.release();
+        ref.clear();
         activeObjects.remove(ref);
     }
         
@@ -89,15 +85,6 @@ public class OpenCLObjectManager {
             return; //nothing to do
         }
         
-        gcCounter++;
-        if (gcCounter >= GC_FREQUENCY) {
-            //The program is that the OpenCLObjects are so small that they are 
-            //enqueued for finalization very late. Therefore, without this
-            //hack, we are running out of host memory on the OpenCL side quickly.
-            gcCounter = 0;
-            Runtime.getRuntime().gc();
-        }
-        
         int removed = 0;
         while (true) {
             // Remove objects reclaimed by GC.
@@ -117,6 +104,7 @@ public class OpenCLObjectManager {
         for (OpenCLObjectRef ref : activeObjects) {
             LOG.log(LOG_LEVEL1, "deleting OpenCL object by: {0}", ref.releaser);
             ref.releaser.release();
+            ref.clear();
         }
         activeObjects.clear();
     }

+ 2 - 5
jme3-core/src/main/java/com/jme3/post/Filter.java

@@ -213,13 +213,10 @@ public abstract class Filter implements Savable {
     }
 
     /**
-     * returns the default pass texture format
-     * default is {@link Format#RGB111110F}
-     * 
-     * @return
+     * returns the default pass texture format.
      */
     protected Format getDefaultPassTextureFormat() {
-        return Format.RGB111110F;
+        return processor.getDefaultPassTextureFormat();
     }
 
     /**

+ 17 - 8
jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java

@@ -66,7 +66,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     private FrameBuffer renderFrameBuffer;
     private Texture2D filterTexture;
     private Texture2D depthTexture;
-    private SafeArrayList<Filter> filters = new SafeArrayList<Filter>(Filter.class);
+    private SafeArrayList<Filter> filters = new SafeArrayList<>(Filter.class);
     private AssetManager assetManager;        
     private Picture fsQuad;
     private boolean computeDepth = false;
@@ -85,6 +85,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     private AppProfiler prof;
 
     private Format fbFormat = Format.RGB111110F;
+    private Format depthFormat = Format.Depth;
     
     /**
      * Create a FilterProcessor 
@@ -143,9 +144,13 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         fsQuad = new Picture("filter full screen quad");
         fsQuad.setWidth(1);
         fsQuad.setHeight(1);
-        
-        if (fbFormat == Format.RGB111110F && !renderer.getCaps().contains(Caps.PackedFloatTexture)) {
-            fbFormat = Format.RGB8;
+
+        if (!renderer.getCaps().contains(Caps.PackedFloatTexture)) {
+            if (!renderer.getCaps().contains(Caps.FloatTexture)) {
+                fbFormat = Format.RGB8;
+            } else {
+                fbFormat = Format.RGB16F;
+            }
         }
         
         Camera cam = vp.getCamera();
@@ -161,6 +166,10 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         reshape(vp, cam.getWidth(), cam.getHeight());
     }
 
+    public Format getDefaultPassTextureFormat() {
+        return fbFormat;
+    }
+    
     /**
      * init the given filter
      * @param filter
@@ -170,7 +179,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         filter.setProcessor(this);
         if (filter.isRequiresDepthTexture()) {
             if (!computeDepth && renderFrameBuffer != null) {
-                depthTexture = new Texture2D(width, height, Format.Depth24);
+                depthTexture = new Texture2D(width, height, depthFormat);
                 renderFrameBuffer.setDepthTexture(depthTexture);
             }
             computeDepth = true;
@@ -469,20 +478,20 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
             renderFrameBufferMS = new FrameBuffer(width, height, numSamples);
             if (caps.contains(Caps.OpenGL32)) {
                 Texture2D msColor = new Texture2D(width, height, numSamples, fbFormat);
-                Texture2D msDepth = new Texture2D(width, height, numSamples, Format.Depth);
+                Texture2D msDepth = new Texture2D(width, height, numSamples, depthFormat);
                 renderFrameBufferMS.setDepthTexture(msDepth);
                 renderFrameBufferMS.setColorTexture(msColor);
                 filterTexture = msColor;
                 depthTexture = msDepth;
             } else {
-                renderFrameBufferMS.setDepthBuffer(Format.Depth);
+                renderFrameBufferMS.setDepthBuffer(depthFormat);
                 renderFrameBufferMS.setColorBuffer(fbFormat);
             }
         }
 
         if (numSamples <= 1 || !caps.contains(Caps.OpenGL32)) {
             renderFrameBuffer = new FrameBuffer(width, height, 1);
-            renderFrameBuffer.setDepthBuffer(Format.Depth);
+            renderFrameBuffer.setDepthBuffer(depthFormat);
             filterTexture = new Texture2D(width, height, fbFormat);
             renderFrameBuffer.setColorTexture(filterTexture);
         }

+ 12 - 10
jme3-core/src/main/java/com/jme3/renderer/Camera.java

@@ -440,23 +440,25 @@ public class Camera implements Savable, Cloneable {
     }
 
     /**
-     * Resizes this camera's view with the given width and height. This is
-     * similar to constructing a new camera, but reusing the same Object. This
-     * method is called by an associated {@link RenderManager} to notify the camera of
-     * changes in the display dimensions.
+     * Resize this camera's view for the specified display size. Invoked by an
+     * associated {@link RenderManager} to notify the camera of changes to the
+     * display dimensions.
      *
-     * @param width the view width
-     * @param height the view height
-     * @param fixAspect If true, the camera's aspect ratio will be recomputed.
-     * Recomputing the aspect ratio requires changing the frustum values.
+     * @param width the new width of the display, in pixels
+     * @param height the new height of the display, in pixels
+     * @param fixAspect if true, recompute the camera's frustum to preserve its
+     * prior aspect ratio
      */
     public void resize(int width, int height, boolean fixAspect) {
         this.width = width;
         this.height = height;
         onViewPortChange();
 
-        if (fixAspect /*&& !parallelProjection*/) {
-            frustumRight = frustumTop * ((float) width / height);
+        if (fixAspect) {
+            float h = height * (viewPortTop - viewPortBottom);
+            float w = width * (viewPortRight - viewPortLeft);
+            float aspectRatio = w / h;
+            frustumRight = frustumTop * aspectRatio;
             frustumLeft = -frustumRight;
             onFrustumChange();
         }

+ 52 - 0
jme3-core/src/main/java/com/jme3/renderer/Caps.java

@@ -128,6 +128,26 @@ public enum Caps {
      * Supports OpenGL 4.0
      */
     OpenGL40,
+    /**
+     * Supports OpenGL 4.1
+     */
+    OpenGL41,
+    /**
+     * Supports OpenGL 4.2
+     */
+    OpenGL42,
+    /**
+     * Supports OpenGL 4.3
+     */
+    OpenGL43,
+    /**
+     * Supports OpenGL 4.4
+     */
+    OpenGL44,
+    /**
+     * Supports OpenGL 4.5
+     */
+    OpenGL45,
     /**
      * Do not use.
      * 
@@ -174,6 +194,26 @@ public enum Caps {
      * Supports GLSL 4.0
      */
     GLSL400,
+    /**
+     * Supports GLSL 4.1
+     */
+    GLSL410,
+    /**
+     * Supports GLSL 4.2
+     */
+    GLSL420,
+    /**
+     * Supports GLSL 4.3
+     */
+    GLSL430,
+    /**
+     * Supports GLSL 4.4
+     */
+    GLSL440,
+    /**
+     * Supports GLSL 4.5
+     */
+    GLSL450,
     /**
      * Supports reading from textures inside the vertex shader.
      */
@@ -486,6 +526,18 @@ public enum Caps {
                         if (!caps.contains(Caps.GLSL150)) return false;
                     case 330:
                         if (!caps.contains(Caps.GLSL330)) return false;
+                    case 400:
+                        if (!caps.contains(Caps.GLSL400)) return false;
+                    case 410:
+                        if (!caps.contains(Caps.GLSL410)) return false;
+                    case 420:
+                        if (!caps.contains(Caps.GLSL420)) return false;
+                    case 430:
+                        if (!caps.contains(Caps.GLSL430)) return false;
+                    case 440:
+                        if (!caps.contains(Caps.GLSL440)) return false;
+                    case 450:
+                        if (!caps.contains(Caps.GLSL450)) return false;
                     default:
                         return false;
                 }

+ 1 - 2
jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java

@@ -50,6 +50,7 @@ public interface GL {
     public static final int GL_ARRAY_BUFFER = 0x8892;
     public static final int GL_BACK = 0x405;
     public static final int GL_BLEND = 0xBE2;
+    public static final int GL_BLUE = 0x1905;
     public static final int GL_BYTE = 0x1400;
     public static final int GL_CLAMP_TO_EDGE = 0x812F;
     public static final int GL_COLOR_BUFFER_BIT = 0x4000;
@@ -173,9 +174,7 @@ public interface GL {
     public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518;
     public static final int GL_TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519;
     public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A;
-    public static final int GL_TEXTURE_BASE_LEVEL = 0x813C;
     public static final int GL_TEXTURE_MAG_FILTER = 0x2800;
-    public static final int GL_TEXTURE_MAX_LEVEL = 0x813D;
     public static final int GL_TEXTURE_MIN_FILTER = 0x2801;
     public static final int GL_TEXTURE_WRAP_S = 0x2802;
     public static final int GL_TEXTURE_WRAP_T = 0x2803;

+ 2 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GL2.java

@@ -65,6 +65,8 @@ public interface GL2 extends GL {
     public static final int GL_STACK_OVERFLOW = 0x503;
     public static final int GL_STACK_UNDERFLOW = 0x504;
     public static final int GL_TEXTURE_3D = 0x806F;
+    public static final int GL_TEXTURE_BASE_LEVEL = 0x813C;
+    public static final int GL_TEXTURE_MAX_LEVEL = 0x813D;
     public static final int GL_POINT_SPRITE = 0x8861;
     public static final int GL_TEXTURE_COMPARE_FUNC = 0x884D;
     public static final int GL_TEXTURE_COMPARE_MODE = 0x884C;

+ 0 - 1
jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java

@@ -86,7 +86,6 @@ public interface GL3 extends GL2 {
     public void glBindFragDataLocation(int param1, int param2, String param3); /// GL3+
     public void glBindVertexArray(int param1); /// GL3+
     public void glDeleteVertexArrays(IntBuffer arrays); /// GL3+
-    public void glFramebufferTextureLayer(int param1, int param2, int param3, int param4, int param5); /// GL3+
     public void glGenVertexArrays(IntBuffer param1); /// GL3+
     public String glGetString(int param1, int param2); /// GL3+
 }

+ 0 - 6
jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java

@@ -95,12 +95,6 @@ public class GLDebugDesktop extends GLDebugES implements GL2, GL3, GL4 {
         checkError();
     }
 
-    @Override
-    public void glFramebufferTextureLayer(int param1, int param2, int param3, int param4, int param5) {
-        gl3.glFramebufferTextureLayer(param1, param2, param3, param4, param5);
-        checkError();
-    }
-
     public void glBlendEquationSeparate(int colorMode, int alphaMode) {
         gl.glBlendEquationSeparate(colorMode, alphaMode);
         checkError();

+ 7 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugES.java

@@ -598,8 +598,15 @@ public class GLDebugES extends GLDebug implements GL, GLFbo, GLExt {
         return sync;
     }
 
+    @Override
     public void glBlendEquationSeparate(int colorMode, int alphaMode) {
         gl.glBlendEquationSeparate(colorMode, alphaMode);
         checkError();
     }
+    
+    @Override
+    public void glFramebufferTextureLayerEXT(int param1, int param2, int param3, int param4, int param5) {
+        glfbo.glFramebufferTextureLayerEXT(param1, param2, param3, param4, param5);
+        checkError();
+    }
 }

+ 1 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java

@@ -61,6 +61,7 @@ public interface GLExt {
 	public static final int GL_FRAMEBUFFER_SRGB_CAPABLE_EXT = 0x8DBA;
 	public static final int GL_FRAMEBUFFER_SRGB_EXT = 0x8DB9;
 	public static final int GL_HALF_FLOAT_ARB = 0x140B;
+        public static final int GL_HALF_FLOAT_OES = 0x8D61;
 	public static final int GL_LUMINANCE16F_ARB = 0x881E;
 	public static final int GL_LUMINANCE32F_ARB = 0x8818;
 	public static final int GL_LUMINANCE_ALPHA16F_ARB = 0x881F;

+ 1 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLFbo.java

@@ -89,6 +89,7 @@ public interface GLFbo {
     public void glDeleteRenderbuffersEXT(IntBuffer param1);
     public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4);
     public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5);
+    public void glFramebufferTextureLayerEXT(int target, int attachment, int texture, int level, int layer);
     public void glGenFramebuffersEXT(IntBuffer param1);
     public void glGenRenderbuffersEXT(IntBuffer param1);
     public void glGenerateMipmapEXT(int param1);

+ 24 - 9
jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java

@@ -103,14 +103,19 @@ public final class GLImageFormats {
     public static GLImageFormat[][] getFormatsForCaps(EnumSet<Caps> caps) {
         GLImageFormat[][] formatToGL = new GLImageFormat[2][Image.Format.values().length];
         
+        int halfFloatFormat = GLExt.GL_HALF_FLOAT_ARB;
+        if (caps.contains(Caps.OpenGLES20)) {
+            halfFloatFormat = GLExt.GL_HALF_FLOAT_OES;
+        }
+        
         // Core Profile Formats (supported by both OpenGL Core 3.3 and OpenGL ES 3.0+)
         if (caps.contains(Caps.CoreProfile)) {
             formatSwiz(formatToGL,     Format.Alpha8,               GL3.GL_R8,                 GL.GL_RED,       GL.GL_UNSIGNED_BYTE);
             formatSwiz(formatToGL,     Format.Luminance8,           GL3.GL_R8,                 GL.GL_RED,       GL.GL_UNSIGNED_BYTE);
             formatSwiz(formatToGL,     Format.Luminance8Alpha8,     GL3.GL_RG8,                GL3.GL_RG,       GL.GL_UNSIGNED_BYTE);
-            formatSwiz(formatToGL,     Format.Luminance16F,         GL3.GL_R16F,               GL.GL_RED,       GLExt.GL_HALF_FLOAT_ARB);
+            formatSwiz(formatToGL,     Format.Luminance16F,         GL3.GL_R16F,               GL.GL_RED,       halfFloatFormat);
             formatSwiz(formatToGL,     Format.Luminance32F,         GL3.GL_R32F,               GL.GL_RED,       GL.GL_FLOAT);
-            formatSwiz(formatToGL,     Format.Luminance16FAlpha16F, GL3.GL_RG16F,              GL3.GL_RG,       GLExt.GL_HALF_FLOAT_ARB);
+            formatSwiz(formatToGL,     Format.Luminance16FAlpha16F, GL3.GL_RG16F,              GL3.GL_RG,       halfFloatFormat);
             
             formatSrgbSwiz(formatToGL, Format.Luminance8,           GLExt.GL_SRGB8_EXT,        GL.GL_RED,       GL.GL_UNSIGNED_BYTE);
             formatSrgbSwiz(formatToGL, Format.Luminance8Alpha8,     GLExt.GL_SRGB8_ALPHA8_EXT, GL3.GL_RG,       GL.GL_UNSIGNED_BYTE);
@@ -163,6 +168,11 @@ public final class GLImageFormats {
             }
             format(formatToGL, Format.RGB8,             GLExt.GL_RGBA8, GL.GL_RGB,             GL.GL_UNSIGNED_BYTE);
             format(formatToGL, Format.RGBA8,            GLExt.GL_RGBA8, GL.GL_RGBA,            GL.GL_UNSIGNED_BYTE);
+            
+            formatSwiz(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_RGB, GL.GL_UNSIGNED_BYTE);
+            formatSwiz(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE);
+            formatSwiz(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE);
+            formatSwiz(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE);
         } else {
             // Actually, the internal format isn't used for OpenGL ES 2! This is the same as the above..
             if (!caps.contains(Caps.CoreProfile)) {
@@ -182,33 +192,38 @@ public final class GLImageFormats {
         
         if (caps.contains(Caps.FloatTexture)) {
             if (!caps.contains(Caps.CoreProfile)) {
-                format(formatToGL, Format.Luminance16F,         GLExt.GL_LUMINANCE16F_ARB,       GL.GL_LUMINANCE,       GLExt.GL_HALF_FLOAT_ARB);
+                format(formatToGL, Format.Luminance16F,         GLExt.GL_LUMINANCE16F_ARB,       GL.GL_LUMINANCE,       halfFloatFormat);
                 format(formatToGL, Format.Luminance32F,         GLExt.GL_LUMINANCE32F_ARB,       GL.GL_LUMINANCE,       GL.GL_FLOAT);
-                format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, GLExt.GL_HALF_FLOAT_ARB);
+                format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat);
             }
-            format(formatToGL, Format.RGB16F,               GLExt.GL_RGB16F_ARB,             GL.GL_RGB,             GLExt.GL_HALF_FLOAT_ARB);
+            format(formatToGL, Format.RGB16F,               GLExt.GL_RGB16F_ARB,             GL.GL_RGB,             halfFloatFormat);
             format(formatToGL, Format.RGB32F,               GLExt.GL_RGB32F_ARB,             GL.GL_RGB,             GL.GL_FLOAT);
-            format(formatToGL, Format.RGBA16F,              GLExt.GL_RGBA16F_ARB,            GL.GL_RGBA,            GLExt.GL_HALF_FLOAT_ARB);
+            format(formatToGL, Format.RGBA16F,              GLExt.GL_RGBA16F_ARB,            GL.GL_RGBA,            halfFloatFormat);
             format(formatToGL, Format.RGBA32F,              GLExt.GL_RGBA32F_ARB,            GL.GL_RGBA,            GL.GL_FLOAT);
         }
         if (caps.contains(Caps.PackedFloatTexture)) {
             format(formatToGL, Format.RGB111110F,           GLExt.GL_R11F_G11F_B10F_EXT,     GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT);
             if (caps.contains(Caps.FloatTexture)) {
-                format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_HALF_FLOAT_ARB);
+                format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, halfFloatFormat);
             }
         }
         if (caps.contains(Caps.SharedExponentTexture)) {
             format(formatToGL, Format.RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_5_9_9_9_REV_EXT);
             if (caps.contains(Caps.FloatTexture)) {
-                format(formatToGL, Format.RGB16F_to_RGB9E5,     GLExt.GL_RGB9_E5_EXT,         GL.GL_RGB, GLExt.GL_HALF_FLOAT_ARB);
+                format(formatToGL, Format.RGB16F_to_RGB9E5,     GLExt.GL_RGB9_E5_EXT,         GL.GL_RGB, halfFloatFormat);
             }
         }
         
         // Need to check if Caps.DepthTexture is supported prior to using for textures
         // But for renderbuffers its OK.
-        format(formatToGL, Format.Depth,   GL.GL_DEPTH_COMPONENT,    GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE);
         format(formatToGL, Format.Depth16, GL.GL_DEPTH_COMPONENT16,  GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT);
         
+        // NOTE: OpenGL ES 2.0 does not support DEPTH_COMPONENT as internal format -- fallback to 16-bit depth.
+        if (caps.contains(Caps.OpenGLES20)) {
+            format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE);
+        } else {
+            format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE);
+        }
         if (caps.contains(Caps.OpenGL20)) {
             format(formatToGL, Format.Depth24, GL2.GL_DEPTH_COMPONENT24,  GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT);
         }

+ 83 - 44
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -180,23 +180,38 @@ public final class GLRenderer implements Renderer {
             caps.add(Caps.OpenGL20);
             if (oglVer >= 210) {
                 caps.add(Caps.OpenGL21);
-                if (oglVer >= 300) {
-                    caps.add(Caps.OpenGL30);
-                    if (oglVer >= 310) {
-                        caps.add(Caps.OpenGL31);
-                        if (oglVer >= 320) {
-                            caps.add(Caps.OpenGL32);
-                        }
-                        if (oglVer >= 330) {
-                            caps.add(Caps.OpenGL33);
-                            caps.add(Caps.GeometryShader);
-                        }
-                        if (oglVer >= 400) {
-                            caps.add(Caps.OpenGL40);
-                            caps.add(Caps.TesselationShader);
-                        }
-                    }
-                }
+            }
+            if (oglVer >= 300) {
+                caps.add(Caps.OpenGL30);
+            }
+            if (oglVer >= 310) {
+                caps.add(Caps.OpenGL31);
+            }
+            if (oglVer >= 320) {
+                caps.add(Caps.OpenGL32);
+            }
+            if (oglVer >= 330) {
+                caps.add(Caps.OpenGL33);
+                caps.add(Caps.GeometryShader);
+            }
+            if (oglVer >= 400) {
+                caps.add(Caps.OpenGL40);
+                caps.add(Caps.TesselationShader);
+            }
+            if (oglVer >= 410) {
+                caps.add(Caps.OpenGL41);
+            }
+            if (oglVer >= 420) {
+                caps.add(Caps.OpenGL42);
+            }
+            if (oglVer >= 430) {
+                caps.add(Caps.OpenGL43);
+            }
+            if (oglVer >= 440) {
+                caps.add(Caps.OpenGL44);
+            }
+            if (oglVer >= 450) {
+                caps.add(Caps.OpenGL45);
             }
         }
 
@@ -209,6 +224,16 @@ public final class GLRenderer implements Renderer {
                 }
                 // so that future OpenGL revisions wont break jme3
                 // fall through intentional
+            case 450:
+                caps.add(Caps.GLSL450);
+            case 440:
+                caps.add(Caps.GLSL440);
+            case 430:
+                caps.add(Caps.GLSL430);
+            case 420:
+                caps.add(Caps.GLSL420);
+            case 410:
+                caps.add(Caps.GLSL410);
             case 400:
                 caps.add(Caps.GLSL400);
             case 330:
@@ -1556,7 +1581,7 @@ public final class GLRenderer implements Renderer {
                     image.getId(),
                     0);
         } else {
-            gl3.glFramebufferTextureLayer(GLFbo.GL_FRAMEBUFFER_EXT, 
+            glfbo.glFramebufferTextureLayerEXT(GLFbo.GL_FRAMEBUFFER_EXT, 
                     convertAttachmentSlot(rb.getSlot()), 
                     image.getId(), 
                     0,
@@ -1955,7 +1980,13 @@ public final class GLRenderer implements Renderer {
     @SuppressWarnings("fallthrough")
     private void setupTextureParams(int unit, Texture tex) {
         Image image = tex.getImage();
-        int target = convertTextureType(tex.getType(), image != null ? image.getMultiSamples() : 1, -1);
+        int samples = image != null ? image.getMultiSamples() : 1;
+        int target = convertTextureType(tex.getType(), samples, -1);
+
+        if (samples > 1) {
+            bindTextureOnly(target, image, unit);
+            return;
+        }
 
         boolean haveMips = true;
         if (image != null) {
@@ -2158,41 +2189,48 @@ public final class GLRenderer implements Renderer {
         int target = convertTextureType(type, img.getMultiSamples(), -1);
         bindTextureAndUnit(target, img, unit);
 
-        if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) {
-            // Image does not have mipmaps, but they are required.
-            // Generate from base level.
+        int imageSamples = img.getMultiSamples();
 
-            if (!caps.contains(Caps.FrameBuffer) && gl2 != null) {
-                gl2.glTexParameteri(target, GL2.GL_GENERATE_MIPMAP, GL.GL_TRUE);
-                img.setMipmapsGenerated(true);
-            } else {
-                // For OpenGL3 and up.
-                // We'll generate mipmaps via glGenerateMipmapEXT (see below)
+        if (imageSamples <= 1) {
+            if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) {
+                // Image does not have mipmaps, but they are required.
+                // Generate from base level.
+
+                if (!caps.contains(Caps.FrameBuffer) && gl2 != null) {
+                    gl2.glTexParameteri(target, GL2.GL_GENERATE_MIPMAP, GL.GL_TRUE);
+                    img.setMipmapsGenerated(true);
+                } else {
+                    // For OpenGL3 and up.
+                    // We'll generate mipmaps via glGenerateMipmapEXT (see below)
+                }
+            } else if (caps.contains(Caps.OpenGL20)) {
+                if (img.hasMipmaps()) {
+                    // Image already has mipmaps, set the max level based on the 
+                    // number of mipmaps we have.
+                    gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1);
+                } else {
+                    // Image does not have mipmaps and they are not required.
+                    // Specify that that the texture has no mipmaps.
+                    gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, 0);
+                }
             }
-        } else if (img.hasMipmaps()) {
-            // Image already has mipmaps, set the max level based on the 
-            // number of mipmaps we have.
-            gl.glTexParameteri(target, GL.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1);
         } else {
-            // Image does not have mipmaps and they are not required.
-            // Specify that that the texture has no mipmaps.
-            gl.glTexParameteri(target, GL.GL_TEXTURE_MAX_LEVEL, 0);
-        }
+            // Check if graphics card doesn't support multisample textures
+            if (!caps.contains(Caps.TextureMultisample)) {
+                throw new RendererException("Multisample textures are not supported by the video hardware");
+            }
+
+            if (img.isGeneratedMipmapsRequired() || img.hasMipmaps()) {
+                throw new RendererException("Multisample textures do not support mipmaps");
+            }
 
-        int imageSamples = img.getMultiSamples();
-        if (imageSamples > 1) {
             if (img.getFormat().isDepthFormat()) {
                 img.setMultiSamples(Math.min(limits.get(Limits.DepthTextureSamples), imageSamples));
             } else {
                 img.setMultiSamples(Math.min(limits.get(Limits.ColorTextureSamples), imageSamples));
             }
-        }
 
-        // Check if graphics card doesn't support multisample textures
-        if (!caps.contains(Caps.TextureMultisample)) {
-            if (img.getMultiSamples() > 1) {
-                throw new RendererException("Multisample textures are not supported by the video hardware");
-            }
+            scaleToPot = false;
         }
 
         // Check if graphics card doesn't support depth textures
@@ -2567,6 +2605,7 @@ public final class GLRenderer implements Renderer {
         }
 
         switch (indexBuf.getFormat()) {
+            case UnsignedByte:
             case UnsignedShort:
                 // OK: Works on all platforms.
                 break;

+ 14 - 10
jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java

@@ -99,6 +99,7 @@ public final class GLTracer implements InvocationHandler {
         noEnumArgs("glEnableVertexAttribArray", 0);
         noEnumArgs("glDisableVertexAttribArray", 0);
         noEnumArgs("glVertexAttribPointer", 0, 1, 4, 5);
+        noEnumArgs("glVertexAttribDivisorARB", 0, 1);
         noEnumArgs("glDrawRangeElements", 1, 2, 3, 5);
         noEnumArgs("glDrawArrays", 1, 2);
         noEnumArgs("glDeleteBuffers", 0);
@@ -111,6 +112,7 @@ public final class GLTracer implements InvocationHandler {
         noEnumArgs("glRenderbufferStorageMultisampleEXT", 1, 3, 4);
         noEnumArgs("glFramebufferRenderbufferEXT", 3);
         noEnumArgs("glFramebufferTexture2DEXT", 3, 4);
+        noEnumArgs("glFramebufferTextureLayerEXT", 2, 3, 4);
         noEnumArgs("glBlitFramebufferEXT", 0, 1, 2, 3, 4, 5, 6, 7, 8);
         
         noEnumArgs("glCreateProgram", -1);
@@ -165,16 +167,17 @@ public final class GLTracer implements InvocationHandler {
     
     /**
      * Creates a tracer implementation that wraps OpenGL ES 2.
-     * 
+     *
      * @param glInterface OGL object to wrap
-     * @param glInterfaceClass The interface to implement
+     * @param glInterfaceClasses The interface(s) to implement
      * @return A tracer that implements the given interface
      */
-    public static Object createGlesTracer(Object glInterface, Class<?> glInterfaceClass) {
-        IntMap<String> constMap = generateConstantMap(GL.class, GLFbo.class, GLExt.class);
-        return Proxy.newProxyInstance(glInterface.getClass().getClassLoader(),
-                                      new Class<?>[] { glInterfaceClass }, 
-                                      new GLTracer(glInterface, constMap));
+    public static Object createGlesTracer(Object glInterface, Class<?>... glInterfaceClasses) {
+        IntMap<String> constMap = generateConstantMap(GL.class, GL2.class, GL3.class, GLFbo.class, GLExt.class);
+        return Proxy.newProxyInstance(
+                glInterface.getClass().getClassLoader(),
+                glInterfaceClasses,
+                new GLTracer(glInterface, constMap));
     }
 
     /**
@@ -301,7 +304,8 @@ public final class GLTracer implements InvocationHandler {
             // will be printed in darker color
             methodName = methodName.substring(2);
             if (methodName.equals("Clear")
-                    || methodName.equals("DrawRangeElements")) {
+                    || methodName.equals("DrawRangeElements")
+                    || methodName.equals("DrawElementsInstancedARB")) {
                 print(methodName);
             } else {
                 if (methodName.endsWith("EXT")) {
@@ -363,8 +367,8 @@ public final class GLTracer implements InvocationHandler {
         printEnum(param);
         print(", ");
         
-        if (param == GL.GL_TEXTURE_BASE_LEVEL
-                || param == GL.GL_TEXTURE_MAX_LEVEL) {
+        if (param == GL2.GL_TEXTURE_BASE_LEVEL
+                || param == GL2.GL_TEXTURE_MAX_LEVEL) {
             printInt(value);
         } else {
             printEnum(value);

+ 9 - 2
jme3-core/src/main/java/com/jme3/renderer/opengl/TextureUtil.java

@@ -32,7 +32,6 @@
 package com.jme3.renderer.opengl;
 
 import com.jme3.renderer.Caps;
-import com.jme3.renderer.RenderContext;
 import com.jme3.renderer.RendererException;
 import com.jme3.texture.Image;
 import com.jme3.texture.Image.Format;
@@ -91,7 +90,7 @@ final class TextureUtil {
 
     public GLImageFormat getImageFormatWithError(Format fmt, boolean isSrgb) {
         //if the passed format is one kind of depth there isno point in getting the srgb format;
-        isSrgb = isSrgb && fmt != Format.Depth && fmt != Format.Depth16 && fmt != Format.Depth24 && fmt != Format.Depth24Stencil8 && fmt != Format.Depth32 && fmt != Format.Depth32F;
+        isSrgb = isSrgb && !fmt.isDepthFormat();
         GLImageFormat glFmt = getImageFormat(fmt, isSrgb);
         if (glFmt == null && isSrgb) {
             glFmt = getImageFormat(fmt, false);               
@@ -127,6 +126,14 @@ final class TextureUtil {
                 gl.glTexParameteri(target, GL3.GL_TEXTURE_SWIZZLE_B, GL.GL_RED);
                 gl.glTexParameteri(target, GL3.GL_TEXTURE_SWIZZLE_A, GL.GL_GREEN);
                 break;
+            case ABGR8:
+                gl.glTexParameteri(target, GL3.GL_TEXTURE_SWIZZLE_R, GL.GL_ALPHA);
+                gl.glTexParameteri(target, GL3.GL_TEXTURE_SWIZZLE_G, GL.GL_BLUE);
+                gl.glTexParameteri(target, GL3.GL_TEXTURE_SWIZZLE_B, GL.GL_GREEN);
+                gl.glTexParameteri(target, GL3.GL_TEXTURE_SWIZZLE_A, GL.GL_RED);
+                break;
+            default:
+                throw new UnsupportedOperationException();
         }
     }
     

+ 14 - 16
jme3-core/src/main/java/com/jme3/scene/AssetLinkNode.java

@@ -31,16 +31,15 @@
  */
 package com.jme3.scene;
 
-import com.jme3.asset.AssetInfo;
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.ModelKey;
 import com.jme3.export.InputCapsule;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
-import com.jme3.export.binary.BinaryImporter;
-import com.jme3.util.clone.Cloner;
 import com.jme3.util.SafeArrayList;
+import com.jme3.util.clone.Cloner;
+
 import java.io.IOException;
 import java.util.*;
 import java.util.Map.Entry;
@@ -164,25 +163,24 @@ public class AssetLinkNode extends Node {
     @Override
     public void read(JmeImporter e) throws IOException {
         super.read(e);
-        InputCapsule capsule = e.getCapsule(this);
-        BinaryImporter importer = BinaryImporter.getInstance();
-        AssetManager loaderManager = e.getAssetManager();
+
+        final InputCapsule capsule = e.getCapsule(this);
+        final AssetManager assetManager = e.getAssetManager();
 
         assetLoaderKeys = (ArrayList<ModelKey>) capsule.readSavableArrayList("assetLoaderKeyList", new ArrayList<ModelKey>());
-        for (Iterator<ModelKey> it = assetLoaderKeys.iterator(); it.hasNext();) {
-            ModelKey modelKey = it.next();
-            AssetInfo info = loaderManager.locateAsset(modelKey);
-            Spatial child = null;
-            if (info != null) {
-                child = (Spatial) importer.load(info);
-            }
+
+        for (final Iterator<ModelKey> iterator = assetLoaderKeys.iterator(); iterator.hasNext(); ) {
+
+            final ModelKey modelKey = iterator.next();
+            final Spatial child = assetManager.loadAsset(modelKey);
+
             if (child != null) {
                 child.parent = this;
                 children.add(child);
                 assetChildren.put(modelKey, child);
             } else {
-                Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Cannot locate {0} for asset link node {1}",
-                                                                    new Object[]{ modelKey, key });
+                Logger.getLogger(this.getClass().getName()).log(Level.WARNING,
+                        "Cannot locate {0} for asset link node {1}", new Object[]{modelKey, key});
             }
         }
     }
@@ -190,7 +188,7 @@ public class AssetLinkNode extends Node {
     @Override
     public void write(JmeExporter e) throws IOException {
         SafeArrayList<Spatial> childs = children;
-        children = new SafeArrayList<Spatial>(Spatial.class);
+        children = new SafeArrayList<>(Spatial.class);
         super.write(e);
         OutputCapsule capsule = e.getCapsule(this);
         capsule.writeSavableArrayList(assetLoaderKeys, "assetLoaderKeyList", null);

+ 102 - 129
jme3-core/src/main/java/com/jme3/scene/BatchNode.java

@@ -62,9 +62,9 @@ import com.jme3.util.clone.JmeCloneable;
  * Sub geoms can be removed but it may be slower than the normal spatial removing
  * Sub geoms can be added after the batch() method has been called but won't be batched and will just be rendered as normal geometries.
  * To integrate them in the batch you have to call the batch() method again on the batchNode.
- *
- * TODO normal or tangents or both looks a bit weird
+ * <p>
  * TODO more automagic (batch when needed in the updateLogicalState)
+ *
  * @author Nehon
  */
 public class BatchNode extends GeometryGroupNode {
@@ -108,7 +108,7 @@ public class BatchNode extends GeometryGroupNode {
     public void onMaterialChange(Geometry geom) {
         throw new UnsupportedOperationException(
                 "Cannot set the material of a batched geometry, "
-                + "change the material of the parent BatchNode.");
+                        + "change the material of the parent BatchNode.");
     }
 
     @Override
@@ -122,7 +122,7 @@ public class BatchNode extends GeometryGroupNode {
         setNeedsFullRebatch(true);
     }
 
-    protected Matrix4f getTransformMatrix(Geometry g){
+    protected Matrix4f getTransformMatrix(Geometry g) {
         return g.cachedWorldMat;
     }
 
@@ -133,35 +133,44 @@ public class BatchNode extends GeometryGroupNode {
             Mesh origMesh = bg.getMesh();
 
             VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
-            FloatBuffer posBuf = (FloatBuffer) pvb.getData();
             VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal);
-            FloatBuffer normBuf = (FloatBuffer) nvb.getData();
+            VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent);
 
             VertexBuffer opvb = origMesh.getBuffer(VertexBuffer.Type.Position);
-            FloatBuffer oposBuf = (FloatBuffer) opvb.getData();
             VertexBuffer onvb = origMesh.getBuffer(VertexBuffer.Type.Normal);
-            FloatBuffer onormBuf = (FloatBuffer) onvb.getData();
+            VertexBuffer otvb = origMesh.getBuffer(VertexBuffer.Type.Tangent);
+
+            FloatBuffer posBuf = getFloatBuffer(pvb);
+            FloatBuffer normBuf = getFloatBuffer(nvb);
+            FloatBuffer tanBuf = getFloatBuffer(tvb);
+
+            FloatBuffer oposBuf = getFloatBuffer(opvb);
+            FloatBuffer onormBuf = getFloatBuffer(onvb);
+            FloatBuffer otanBuf = getFloatBuffer(otvb);
+
             Matrix4f transformMat = getTransformMatrix(bg);
+            doTransforms(oposBuf, onormBuf, otanBuf, posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
 
-            if (mesh.getBuffer(VertexBuffer.Type.Tangent) != null) {
+            pvb.updateData(posBuf);
 
-                VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent);
-                FloatBuffer tanBuf = (FloatBuffer) tvb.getData();
-                VertexBuffer otvb = origMesh.getBuffer(VertexBuffer.Type.Tangent);
-                FloatBuffer otanBuf = (FloatBuffer) otvb.getData();
-                doTransformsTangents(oposBuf, onormBuf, otanBuf, posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
+            if (nvb != null) {
+                nvb.updateData(normBuf);
+            }
+            if (tvb != null) {
                 tvb.updateData(tanBuf);
-            } else {
-                doTransforms(oposBuf, onormBuf, posBuf, normBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
             }
-            pvb.updateData(posBuf);
-            nvb.updateData(normBuf);
-
 
             batch.geometry.updateModelBound();
         }
     }
 
+    private FloatBuffer getFloatBuffer(VertexBuffer vb) {
+        if (vb == null) {
+            return null;
+        }
+        return (FloatBuffer) vb.getData();
+    }
+
     /**
      * Batch this batchNode
      * every geometry of the sub scene graph of this node will be batched into a single mesh that will be rendered in one call
@@ -234,7 +243,7 @@ public class BatchNode extends GeometryGroupNode {
         logger.log(Level.FINE, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()});
 
         //init the temp arrays if something has been batched only.
-        if(matMap.size()>0){
+        if (matMap.size() > 0) {
             //TODO these arrays should be allocated by chunk instead to avoid recreating them each time the batch is changed.
             //init temp float arrays
             tmpFloat = new float[maxVertCount * 3];
@@ -257,6 +266,7 @@ public class BatchNode extends GeometryGroupNode {
 
     /**
      * recursively visit the subgraph and unbatch geometries
+     *
      * @param s
      */
     private void unbatchSubGraph(Spatial s) {
@@ -343,11 +353,10 @@ public class BatchNode extends GeometryGroupNode {
 
     /**
      * Returns the material that is used for the first batch of this BatchNode
-     *
+     * <p>
      * use getMaterial(Material material,int batchIndex) to get a material from a specific batch
      *
      * @return the material that is used for the first batch of this BatchNode
-     *
      * @see #setMaterial(com.jme3.material.Material)
      */
     public Material getMaterial() {
@@ -428,14 +437,6 @@ public class BatchNode extends GeometryGroupNode {
                         + " primitive types: " + mode + " != " + listMode);
             }
             mode = listMode;
-            //Not needed anymore as lineWidth is now in RenderState and will be taken into account when merging according to the material
-//            if (mode == Mesh.Mode.Lines) {
-//                if (lineWidth != 1f && listLineWidth != lineWidth) {
-//                    throw new UnsupportedOperationException("When using Mesh Line mode, cannot combine meshes with different line width "
-//                            + lineWidth + " != " + listLineWidth);
-//                }
-//                lineWidth = listLineWidth;
-//            }
             compsForBuf[VertexBuffer.Type.Index.ordinal()] = components;
         }
 
@@ -528,53 +529,7 @@ public class BatchNode extends GeometryGroupNode {
         }
     }
 
-    private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bufPos, FloatBuffer bufNorm, int start, int end, Matrix4f transform) {
-        TempVars vars = TempVars.get();
-        Vector3f pos = vars.vect1;
-        Vector3f norm = vars.vect2;
-
-        int length = (end - start) * 3;
-
-        // offset is given in element units
-        // convert to be in component units
-        int offset = start * 3;
-        bindBufPos.rewind();
-        bindBufNorm.rewind();
-        //bufPos.position(offset);
-        //bufNorm.position(offset);
-        bindBufPos.get(tmpFloat, 0, length);
-        bindBufNorm.get(tmpFloatN, 0, length);
-        int index = 0;
-        while (index < length) {
-            pos.x = tmpFloat[index];
-            norm.x = tmpFloatN[index++];
-            pos.y = tmpFloat[index];
-            norm.y = tmpFloatN[index++];
-            pos.z = tmpFloat[index];
-            norm.z = tmpFloatN[index];
-
-            transform.mult(pos, pos);
-            transform.multNormal(norm, norm);
-
-            index -= 2;
-            tmpFloat[index] = pos.x;
-            tmpFloatN[index++] = norm.x;
-            tmpFloat[index] = pos.y;
-            tmpFloatN[index++] = norm.y;
-            tmpFloat[index] = pos.z;
-            tmpFloatN[index++] = norm.z;
-
-        }
-        vars.release();
-        bufPos.position(offset);
-        //using bulk put as it's faster
-        bufPos.put(tmpFloat, 0, length);
-        bufNorm.position(offset);
-        //using bulk put as it's faster
-        bufNorm.put(tmpFloatN, 0, length);
-    }
-
-    private void doTransformsTangents(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bindBufTangents,FloatBuffer bufPos, FloatBuffer bufNorm, FloatBuffer bufTangents, int start, int end, Matrix4f transform) {
+    private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bindBufTangents, FloatBuffer bufPos, FloatBuffer bufNorm, FloatBuffer bufTangents, int start, int end, Matrix4f transform) {
         TempVars vars = TempVars.get();
         Vector3f pos = vars.vect1;
         Vector3f norm = vars.vect2;
@@ -588,60 +543,76 @@ public class BatchNode extends GeometryGroupNode {
         int offset = start * 3;
         int tanOffset = start * 4;
 
-
         bindBufPos.rewind();
-        bindBufNorm.rewind();
-        bindBufTangents.rewind();
         bindBufPos.get(tmpFloat, 0, length);
-        bindBufNorm.get(tmpFloatN, 0, length);
-        bindBufTangents.get(tmpFloatT, 0, tanLength);
+
+        if (bindBufNorm != null) {
+            bindBufNorm.rewind();
+            bindBufNorm.get(tmpFloatN, 0, length);
+        }
+
+        if (bindBufTangents != null) {
+            bindBufTangents.rewind();
+            bindBufTangents.get(tmpFloatT, 0, tanLength);
+        }
 
         int index = 0;
         int tanIndex = 0;
-        while (index < length) {
-            pos.x = tmpFloat[index];
-            norm.x = tmpFloatN[index++];
-            pos.y = tmpFloat[index];
-            norm.y = tmpFloatN[index++];
-            pos.z = tmpFloat[index];
-            norm.z = tmpFloatN[index];
+        int index1, index2, tanIndex1, tanIndex2;
 
-            tan.x = tmpFloatT[tanIndex++];
-            tan.y = tmpFloatT[tanIndex++];
-            tan.z = tmpFloatT[tanIndex++];
+        while (index < length) {
+            index1 = index + 1;
+            index2 = index + 2;
 
+            pos.x = tmpFloat[index];
+            pos.y = tmpFloat[index1];
+            pos.z = tmpFloat[index2];
             transform.mult(pos, pos);
-            transform.multNormal(norm, norm);
-            transform.multNormal(tan, tan);
-
-            index -= 2;
-            tanIndex -= 3;
-
             tmpFloat[index] = pos.x;
-            tmpFloatN[index++] = norm.x;
-            tmpFloat[index] = pos.y;
-            tmpFloatN[index++] = norm.y;
-            tmpFloat[index] = pos.z;
-            tmpFloatN[index++] = norm.z;
-
-            tmpFloatT[tanIndex++] = tan.x;
-            tmpFloatT[tanIndex++] = tan.y;
-            tmpFloatT[tanIndex++] = tan.z;
+            tmpFloat[index1] = pos.y;
+            tmpFloat[index2] = pos.z;
+
+            if (bindBufNorm != null) {
+                norm.x = tmpFloatN[index];
+                norm.y = tmpFloatN[index1];
+                norm.z = tmpFloatN[index2];
+                transform.multNormal(norm, norm);
+                tmpFloatN[index] = norm.x;
+                tmpFloatN[index1] = norm.y;
+                tmpFloatN[index2] = norm.z;
+            }
 
-            //Skipping 4th element of tangent buffer (handedness)
-            tanIndex++;
+            index += 3;
+
+            if (bindBufTangents != null) {
+                tanIndex1 = tanIndex + 1;
+                tanIndex2 = tanIndex + 2;
+                tan.x = tmpFloatT[tanIndex];
+                tan.y = tmpFloatT[tanIndex1];
+                tan.z = tmpFloatT[tanIndex2];
+                transform.multNormal(tan, tan);
+                tmpFloatT[tanIndex] = tan.x;
+                tmpFloatT[tanIndex1] = tan.y;
+                tmpFloatT[tanIndex2] = tan.z;
+                tanIndex += 4;
+            }
 
         }
         vars.release();
-        bufPos.position(offset);
+
         //using bulk put as it's faster
+        bufPos.position(offset);
         bufPos.put(tmpFloat, 0, length);
-        bufNorm.position(offset);
-        //using bulk put as it's faster
-        bufNorm.put(tmpFloatN, 0, length);
-        bufTangents.position(tanOffset);
-        //using bulk put as it's faster
-        bufTangents.put(tmpFloatT, 0, tanLength);
+
+        if (bindBufNorm != null) {
+            bufNorm.position(offset);
+            bufNorm.put(tmpFloatN, 0, length);
+        }
+
+        if (bindBufTangents != null) {
+            bufTangents.position(tanOffset);
+            bufTangents.put(tmpFloatT, 0, tanLength);
+        }
     }
 
     private void doCopyBuffer(FloatBuffer inBuf, int offset, FloatBuffer outBuf, int componentSize) {
@@ -653,11 +624,11 @@ public class BatchNode extends GeometryGroupNode {
         offset *= componentSize;
 
         for (int i = 0; i < inBuf.limit() / componentSize; i++) {
-            pos.x = inBuf.get(i * componentSize + 0);
+            pos.x = inBuf.get(i * componentSize);
             pos.y = inBuf.get(i * componentSize + 1);
             pos.z = inBuf.get(i * componentSize + 2);
 
-            outBuf.put(offset + i * componentSize + 0, pos.x);
+            outBuf.put(offset + i * componentSize, pos.x);
             outBuf.put(offset + i * componentSize + 1, pos.y);
             outBuf.put(offset + i * componentSize + 2, pos.z);
         }
@@ -667,6 +638,7 @@ public class BatchNode extends GeometryGroupNode {
     protected class Batch implements JmeCloneable {
         /**
          * update the batchesByGeom map for this batch with the given List of geometries
+         *
          * @param list
          */
         void updateGeomList(List<Geometry> list) {
@@ -676,6 +648,7 @@ public class BatchNode extends GeometryGroupNode {
                 }
             }
         }
+
         Geometry geometry;
 
         public final Geometry getGeometry() {
@@ -685,14 +658,14 @@ public class BatchNode extends GeometryGroupNode {
         @Override
         public Batch jmeClone() {
             try {
-                return (Batch)super.clone();
+                return (Batch) super.clone();
             } catch (CloneNotSupportedException ex) {
                 throw new AssertionError();
             }
         }
 
         @Override
-        public void cloneFields( Cloner cloner, Object original ) {
+        public void cloneFields(Cloner cloner, Object original) {
             this.geometry = cloner.clone(geometry);
         }
 
@@ -704,11 +677,11 @@ public class BatchNode extends GeometryGroupNode {
 
     @Override
     public Node clone(boolean cloneMaterials) {
-        BatchNode clone = (BatchNode)super.clone(cloneMaterials);
-        if ( batches.size() > 0) {
-            for ( Batch b : batches ) {
-                for ( int i =0; i < clone.children.size(); i++ ) {
-                    if ( clone.children.get(i).getName().equals(b.geometry.getName())) {
+        BatchNode clone = (BatchNode) super.clone(cloneMaterials);
+        if (batches.size() > 0) {
+            for (Batch b : batches) {
+                for (int i = 0; i < clone.children.size(); i++) {
+                    if (clone.children.get(i).getName().equals(b.geometry.getName())) {
                         clone.children.remove(i);
                         break;
                     }
@@ -723,10 +696,10 @@ public class BatchNode extends GeometryGroupNode {
     }
 
     /**
-     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     * Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
      */
     @Override
-    public void cloneFields( Cloner cloner, Object original ) {
+    public void cloneFields(Cloner cloner, Object original) {
         super.cloneFields(cloner, original);
 
         this.batches = cloner.clone(batches);
@@ -736,7 +709,7 @@ public class BatchNode extends GeometryGroupNode {
 
 
         HashMap<Geometry, Batch> newBatchesByGeom = new HashMap<Geometry, Batch>();
-        for( Map.Entry<Geometry, Batch> e : batchesByGeom.entrySet() ) {
+        for (Map.Entry<Geometry, Batch> e : batchesByGeom.entrySet()) {
             newBatchesByGeom.put(cloner.clone(e.getKey()), cloner.clone(e.getValue()));
         }
         this.batchesByGeom = newBatchesByGeom;
@@ -745,7 +718,7 @@ public class BatchNode extends GeometryGroupNode {
     @Override
     public int collideWith(Collidable other, CollisionResults results) {
         int total = 0;
-        for (Spatial child : children.getArray()){
+        for (Spatial child : children.getArray()) {
             if (!isBatch(child)) {
                 total += child.collideWith(other, results);
             }

+ 123 - 122
jme3-core/src/main/java/com/jme3/scene/CameraNode.java

@@ -1,122 +1,123 @@
-/*
- * 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;
-
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.renderer.Camera;
-import com.jme3.scene.control.CameraControl;
-import com.jme3.scene.control.CameraControl.ControlDirection;
-import com.jme3.util.clone.Cloner;
-import java.io.IOException;
-
-/**
- * <code>CameraNode</code> simply uses {@link CameraControl} to implement
- * linking of camera and node data.
- *
- * @author Tim8Dev
- */
-public class CameraNode extends Node {
-
-    private CameraControl camControl;
-
-    /**
-     * Serialization only. Do not use.
-     */
-    public CameraNode() {
-    }
-
-    public CameraNode(String name, Camera camera) {
-        this(name, new CameraControl(camera));
-    }
-
-    public CameraNode(String name, CameraControl control) {
-        super(name);
-        addControl(control);
-        camControl = control;
-    }
-
-    public void setEnabled(boolean enabled) {
-        camControl.setEnabled(enabled);
-    }
-
-    public boolean isEnabled() {
-        return camControl.isEnabled();
-    }
-
-    public void setControlDir(ControlDirection controlDir) {
-        camControl.setControlDir(controlDir);
-    }
-
-    public void setCamera(Camera camera) {
-        camControl.setCamera(camera);
-    }
-
-    public ControlDirection getControlDir() {
-        return camControl.getControlDir();
-    }
-
-    public Camera getCamera() {
-        return camControl.getCamera();
-    }
-
-//    @Override
-//    public void lookAt(Vector3f position, Vector3f upVector) {
-//        this.lookAt(position, upVector);
-//        camControl.getCamera().lookAt(position, upVector);
-//    }
-
-    /**
-     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
-     */
-    @Override
-    public void cloneFields( Cloner cloner, Object original ) {
-        super.cloneFields(cloner, original);
-
-        // A change in behavior... I think previously CameraNode was probably
-        // not really cloneable... or at least its camControl would be pointing
-        // to the wrong control. -pspeed
-        this.camControl = cloner.clone(camControl);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        camControl = (CameraControl)im.getCapsule(this).readSavable("camControl", null);
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        ex.getCapsule(this).write(camControl, "camControl", null);
-    }
-}
+/*
+ * 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;
+
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.control.CameraControl;
+import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+
+/**
+ * <code>CameraNode</code> simply uses {@link CameraControl} to implement
+ * linking of camera and node data.
+ *
+ * @author Tim8Dev
+ */
+public class CameraNode extends Node {
+
+    private CameraControl camControl;
+
+    /**
+     * Serialization only. Do not use.
+     */
+    public CameraNode() {
+        super();
+    }
+
+    public CameraNode(String name, Camera camera) {
+        this(name, new CameraControl(camera));
+    }
+
+    public CameraNode(String name, CameraControl control) {
+        super(name);
+        addControl(control);
+        camControl = control;
+    }
+
+    public void setEnabled(boolean enabled) {
+        camControl.setEnabled(enabled);
+    }
+
+    public boolean isEnabled() {
+        return camControl.isEnabled();
+    }
+
+    public void setControlDir(ControlDirection controlDir) {
+        camControl.setControlDir(controlDir);
+    }
+
+    public void setCamera(Camera camera) {
+        camControl.setCamera(camera);
+    }
+
+    public ControlDirection getControlDir() {
+        return camControl.getControlDir();
+    }
+
+    public Camera getCamera() {
+        return camControl.getCamera();
+    }
+
+//    @Override
+//    public void lookAt(Vector3f position, Vector3f upVector) {
+//        this.lookAt(position, upVector);
+//        camControl.getCamera().lookAt(position, upVector);
+//    }
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
+        // A change in behavior... I think previously CameraNode was probably
+        // not really cloneable... or at least its camControl would be pointing
+        // to the wrong control. -pspeed
+        this.camControl = cloner.clone(camControl);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        camControl = (CameraControl)im.getCapsule(this).readSavable("camControl", null);
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        ex.getCapsule(this).write(camControl, "camControl", null);
+    }
+}

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

@@ -398,6 +398,9 @@ public class Geometry extends Spatial {
 
         // Compute the cached world matrix
         cachedWorldMat.loadIdentity();
+        if (ignoreTransform) {
+            return;
+        }
         cachedWorldMat.setRotationQuaternion(worldTransform.getRotation());
         cachedWorldMat.setTranslation(worldTransform.getTranslation());
 

+ 94 - 55
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -53,6 +53,7 @@ import com.jme3.util.IntMap.Entry;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.nio.*;
 import java.util.ArrayList;
@@ -331,6 +332,15 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         this.modeStart = cloner.clone(modeStart);
     }
 
+    /**
+     * @param forSoftwareAnim
+     * @deprecated use generateBindPose();
+     */
+    @Deprecated
+    public void generateBindPose(boolean forSoftwareAnim) {
+        generateBindPose();
+    }
+
     /**
      * Generates the {@link Type#BindPosePosition}, {@link Type#BindPoseNormal},
      * and {@link Type#BindPoseTangent}
@@ -338,51 +348,48 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * buffers already set on the mesh.
      * This method does nothing if the mesh has no bone weight or index
      * buffers.
-     *
-     * @param forSoftwareAnim Should be true if the bind pose is to be generated.
      */
-    public void generateBindPose(boolean forSoftwareAnim){
-        if (forSoftwareAnim){
-            VertexBuffer pos = getBuffer(Type.Position);
-            if (pos == null || getBuffer(Type.BoneIndex) == null) {
-                // ignore, this mesh doesn't have positional data
-                // or it doesn't have bone-vertex assignments, so its not animated
-                return;
-            }
-
-            VertexBuffer bindPos = new VertexBuffer(Type.BindPosePosition);
-            bindPos.setupData(Usage.CpuOnly,
-                    pos.getNumComponents(),
-                    pos.getFormat(),
-                    BufferUtils.clone(pos.getData()));
-            setBuffer(bindPos);
-
-            // XXX: note that this method also sets stream mode
-            // so that animation is faster. this is not needed for hardware skinning
-            pos.setUsage(Usage.Stream);
-
-            VertexBuffer norm = getBuffer(Type.Normal);
-            if (norm != null) {
-                VertexBuffer bindNorm = new VertexBuffer(Type.BindPoseNormal);
-                bindNorm.setupData(Usage.CpuOnly,
-                        norm.getNumComponents(),
-                        norm.getFormat(),
-                        BufferUtils.clone(norm.getData()));
-                setBuffer(bindNorm);
-                norm.setUsage(Usage.Stream);
-            }
+    public void generateBindPose() {
+        VertexBuffer pos = getBuffer(Type.Position);
+        if (pos == null || getBuffer(Type.BoneIndex) == null) {
+            // ignore, this mesh doesn't have positional data
+            // or it doesn't have bone-vertex assignments, so its not animated
+            return;
+        }
 
-            VertexBuffer tangents = getBuffer(Type.Tangent);
-            if (tangents != null) {
-                VertexBuffer bindTangents = new VertexBuffer(Type.BindPoseTangent);
-                bindTangents.setupData(Usage.CpuOnly,
-                        tangents.getNumComponents(),
-                        tangents.getFormat(),
-                        BufferUtils.clone(tangents.getData()));
-                setBuffer(bindTangents);
-                tangents.setUsage(Usage.Stream);
-            }// else hardware setup does nothing, mesh already in bind pose
+        VertexBuffer bindPos = new VertexBuffer(Type.BindPosePosition);
+        bindPos.setupData(Usage.CpuOnly,
+                pos.getNumComponents(),
+                pos.getFormat(),
+                BufferUtils.clone(pos.getData()));
+        setBuffer(bindPos);
+
+        // XXX: note that this method also sets stream mode
+        // so that animation is faster. this is not needed for hardware skinning
+        pos.setUsage(Usage.Stream);
+
+        VertexBuffer norm = getBuffer(Type.Normal);
+        if (norm != null) {
+            VertexBuffer bindNorm = new VertexBuffer(Type.BindPoseNormal);
+            bindNorm.setupData(Usage.CpuOnly,
+                    norm.getNumComponents(),
+                    norm.getFormat(),
+                    BufferUtils.clone(norm.getData()));
+            setBuffer(bindNorm);
+            norm.setUsage(Usage.Stream);
         }
+
+        VertexBuffer tangents = getBuffer(Type.Tangent);
+        if (tangents != null) {
+            VertexBuffer bindTangents = new VertexBuffer(Type.BindPoseTangent);
+            bindTangents.setupData(Usage.CpuOnly,
+                    tangents.getNumComponents(),
+                    tangents.getFormat(),
+                    BufferUtils.clone(tangents.getData()));
+            setBuffer(bindTangents);
+            tangents.setUsage(Usage.Stream);
+        }// else hardware setup does nothing, mesh already in bind pose
+
     }
 
     /**
@@ -396,11 +403,20 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             // convert indices to ubytes on the heap
             VertexBuffer indices = getBuffer(Type.BoneIndex);
             if (!indices.getData().hasArray()) {
-                ByteBuffer originalIndex = (ByteBuffer) indices.getData();
-                ByteBuffer arrayIndex = ByteBuffer.allocate(originalIndex.capacity());
-                originalIndex.clear();
-                arrayIndex.put(originalIndex);
-                indices.updateData(arrayIndex);
+                if (indices.getFormat() == Format.UnsignedByte) {
+                    ByteBuffer originalIndex = (ByteBuffer) indices.getData();
+                    ByteBuffer arrayIndex = ByteBuffer.allocate(originalIndex.capacity());
+                    originalIndex.clear();
+                    arrayIndex.put(originalIndex);
+                    indices.updateData(arrayIndex);
+                } else {
+                    //bone indices can be stored in an UnsignedShort buffer
+                    ShortBuffer originalIndex = (ShortBuffer) indices.getData();
+                    ShortBuffer arrayIndex = ShortBuffer.allocate(originalIndex.capacity());
+                    originalIndex.clear();
+                    arrayIndex.put(originalIndex);
+                    indices.updateData(arrayIndex);
+                }
             }
             indices.setUsage(Usage.CpuOnly);
 
@@ -429,13 +445,24 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             //if HWBoneIndex and HWBoneWeight are empty, we setup them as direct
             //buffers with software anim buffers data
             VertexBuffer indicesHW = getBuffer(Type.HWBoneIndex);
+            Buffer result;
             if (indicesHW.getData() == null) {
                 VertexBuffer indices = getBuffer(Type.BoneIndex);
-                ByteBuffer originalIndex = (ByteBuffer) indices.getData();
-                ByteBuffer directIndex = BufferUtils.createByteBuffer(originalIndex.capacity());
-                originalIndex.clear();
-                directIndex.put(originalIndex);
-                indicesHW.setupData(Usage.Static, indices.getNumComponents(), indices.getFormat(), directIndex);
+                if (indices.getFormat() == Format.UnsignedByte) {
+                    ByteBuffer originalIndex = (ByteBuffer) indices.getData();
+                    ByteBuffer directIndex = BufferUtils.createByteBuffer(originalIndex.capacity());
+                    originalIndex.clear();
+                    directIndex.put(originalIndex);
+                    result = directIndex;
+                } else {
+                    //bone indices can be stored in an UnsignedShort buffer
+                    ShortBuffer originalIndex = (ShortBuffer) indices.getData();
+                    ShortBuffer directIndex = BufferUtils.createShortBuffer(originalIndex.capacity());
+                    originalIndex.clear();
+                    directIndex.put(originalIndex);
+                    result = directIndex;
+                }
+                indicesHW.setupData(Usage.Static, indices.getNumComponents(), indices.getFormat(), result);
             }
 
             VertexBuffer weightsHW = getBuffer(Type.HWBoneWeight);
@@ -986,6 +1013,18 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
                            BoundingVolume worldBound,
                            CollisionResults results){
 
+        switch (mode) {
+            case Points:
+            case Lines:
+            case LineStrip:
+            case LineLoop:
+                /*
+                 * Collisions can be detected only with triangles,
+                 * and there are no triangles in this mesh.
+                 */
+                return 0;
+        }
+
         if (getVertexCount() == 0) {
             return 0;
         }
@@ -1420,7 +1459,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             return false; // no bone animation data
         }
 
-        ByteBuffer boneIndexBuffer = (ByteBuffer) biBuf.getData();
+        IndexBuffer boneIndexBuffer = IndexBuffer.wrapIndexBuffer(biBuf.getData());
         boneIndexBuffer.rewind();
         int numBoneIndices = boneIndexBuffer.remaining();
         assert numBoneIndices % 4 == 0 : numBoneIndices;
@@ -1433,10 +1472,10 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         /*
          * Test each vertex to determine whether the bone affects it.
          */
-        byte biByte = (byte) boneIndex; // bone indices wrap after 127
+        int biByte = boneIndex;
         for (int vIndex = 0; vIndex < numVertices; vIndex++) {
             for (int wIndex = 0; wIndex < 4; wIndex++) {
-                byte bIndex = boneIndexBuffer.get();
+                int bIndex = boneIndexBuffer.get();
                 float weight = weightBuffer.get();
                 if (wIndex < maxNumWeights && bIndex == biByte && weight != 0f) {
                     return true;

+ 29 - 24
jme3-core/src/main/java/com/jme3/scene/control/LightControl.java

@@ -42,8 +42,9 @@ import com.jme3.light.SpotLight;
 import com.jme3.math.Vector3f;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
-import com.jme3.scene.Spatial;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+
 import java.io.IOException;
 
 /**
@@ -54,7 +55,10 @@ import java.io.IOException;
  */
 public class LightControl extends AbstractControl {
 
-    public static enum ControlDirection {
+    private static final String CONTROL_DIR_NAME = "controlDir";
+    private static final String LIGHT_NAME = "light";
+
+    public enum ControlDirection {
 
         /**
          * Means, that the Light's transform is "copied"
@@ -67,6 +71,7 @@ public class LightControl extends AbstractControl {
          */
         SpatialToLight;
     }
+
     private Light light;
     private ControlDirection controlDir = ControlDirection.SpatialToLight;
 
@@ -113,7 +118,7 @@ public class LightControl extends AbstractControl {
         if (spatial != null && light != null) {
             switch (controlDir) {
                 case SpatialToLight:
-                    spatialTolight(light);
+                    spatialToLight(light);
                     break;
                 case LightToSpatial:
                     lightToSpatial(light);
@@ -122,22 +127,29 @@ public class LightControl extends AbstractControl {
         }
     }
 
-    private void spatialTolight(Light light) {
+    private void spatialToLight(Light light) {
+
+        final Vector3f worldTranslation = spatial.getWorldTranslation();
+
         if (light instanceof PointLight) {
-            ((PointLight) light).setPosition(spatial.getWorldTranslation());
+            ((PointLight) light).setPosition(worldTranslation);
+            return;
         }
-        TempVars vars = TempVars.get();
+
+        final TempVars vars = TempVars.get();
+        final Vector3f vec = vars.vect1;
 
         if (light instanceof DirectionalLight) {
-            ((DirectionalLight) light).setDirection(vars.vect1.set(spatial.getWorldTranslation()).multLocal(-1.0f));
+            ((DirectionalLight) light).setDirection(vec.set(worldTranslation).multLocal(-1.0f));
         }
 
         if (light instanceof SpotLight) {
-            ((SpotLight) light).setPosition(spatial.getWorldTranslation());            
-            ((SpotLight) light).setDirection(spatial.getWorldRotation().multLocal(vars.vect1.set(Vector3f.UNIT_Y).multLocal(-1)));
+            final SpotLight spotLight = (SpotLight) light;
+            spotLight.setPosition(worldTranslation);
+            spotLight.setDirection(spatial.getWorldRotation().multLocal(vec.set(Vector3f.UNIT_Y).multLocal(-1)));
         }
-        vars.release();
 
+        vars.release();
     }
 
     private void lightToSpatial(Light light) {
@@ -158,8 +170,6 @@ public class LightControl extends AbstractControl {
         }
         vars.release();
         //TODO add code for Spot light here when it's done
-
-
     }
 
     @Override
@@ -167,23 +177,18 @@ public class LightControl extends AbstractControl {
         // nothing to do
     }
 
-    // default implementation from AbstractControl is equivalent
-    //@Override
-    //public Control cloneForSpatial(Spatial newSpatial) {
-    //    LightControl control = new LightControl(light, controlDir);
-    //    control.setSpatial(newSpatial);
-    //    control.setEnabled(isEnabled());
-    //    return control;
-    //}
-    private static final String CONTROL_DIR_NAME = "controlDir";
-    private static final String LIGHT_NAME = "light";
-    
+    @Override
+    public void cloneFields(final Cloner cloner, final Object original) {
+        super.cloneFields(cloner, original);
+        light = cloner.clone(light);
+    }
+
     @Override
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
         controlDir = ic.readEnum(CONTROL_DIR_NAME, ControlDirection.class, ControlDirection.SpatialToLight);
-        light = (Light)ic.readSavable(LIGHT_NAME, null);
+        light = (Light) ic.readSavable(LIGHT_NAME, null);
     }
 
     @Override

+ 4 - 1
jme3-core/src/main/java/com/jme3/scene/debug/Grid.java

@@ -32,9 +32,9 @@
 package com.jme3.scene.debug;
 
 import com.jme3.scene.Mesh;
-import com.jme3.scene.Mesh.Mode;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.util.BufferUtils;
+
 import java.nio.FloatBuffer;
 import java.nio.ShortBuffer;
 
@@ -45,6 +45,9 @@ import java.nio.ShortBuffer;
  */
 public class Grid extends Mesh {
 
+    public Grid() {
+    }
+
     /**
      * Creates a grid debug shape.
      * @param xLines

+ 11 - 9
jme3-core/src/main/java/com/jme3/scene/debug/WireSphere.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2017 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -106,6 +106,9 @@ public class WireSphere extends Mesh {
 //        pb.put(0).put(0).put(radius);
 //        pb.put(0).put(0).put(-radius);
 
+        /*
+         * Update vertex positions for the great circle in the X-Y plane.
+         */
         float rate = FastMath.TWO_PI / (float) samples;
         float angle = 0;
         for (int i = 0; i < samples; i++) {
@@ -114,7 +117,9 @@ public class WireSphere extends Mesh {
             pb.put(x).put(y).put(0);
             angle += rate;
         }
-
+        /*
+         * Update vertex positions for the great circle in the Y-Z plane.
+         */
         angle = 0;
         for (int i = 0; i < samples; i++) {
             float x = radius * FastMath.cos(angle);
@@ -122,23 +127,20 @@ public class WireSphere extends Mesh {
             pb.put(0).put(x).put(y);
             angle += rate;
         }
-
+        /*
+         * Update vertex positions for 'zSamples' parallel circles.
+         */
         float zRate = (radius * 2) / (float) (zSamples);
         float zHeight = -radius + (zRate / 2f);
-
-
         float rb = 1f / zSamples;
         float b = rb / 2f;
-
         for (int k = 0; k < zSamples; k++) {
             angle = 0;
-            float scale = FastMath.sin(b * FastMath.PI);
+            float scale = 2f * FastMath.sqrt(b - b * b);
             for (int i = 0; i < samples; i++) {
                 float x = radius * FastMath.cos(angle);
                 float y = radius * FastMath.sin(angle);
-
                 pb.put(x * scale).put(zHeight).put(y * scale);
-
                 angle += rate;
             }
             zHeight += zRate;

+ 417 - 0
jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java

@@ -0,0 +1,417 @@
+/*
+ * 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.
+ */
+// $Id: Cylinder.java 4131 2009-03-19 20:15:28Z blaine.dev $
+package com.jme3.scene.debug.custom;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.util.BufferUtils;
+
+import static com.jme3.util.BufferUtils.*;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+
+/**
+ * A simple cylinder, defined by it's height and radius.
+ * (Ported to jME3)
+ *
+ * @author Mark Powell
+ * @version $Revision: 4131 $, $Date: 2009-03-19 16:15:28 -0400 (Thu, 19 Mar 2009) $
+ */
+public class BoneShape extends Mesh {
+
+    private int axisSamples;
+
+    private int radialSamples;
+
+    private float radius;
+    private float radius2;
+
+    private float height;
+    private boolean closed;
+    private boolean inverted;
+
+    /**
+     * Default constructor for serialization only. Do not use.
+     */
+    public BoneShape() {
+    }
+
+    /**
+     * Creates a new Cylinder. By default its center is the origin. Usually, a
+     * higher sample number creates a better looking cylinder, but at the cost
+     * of more vertex information.
+     *
+     * @param axisSamples   Number of triangle samples along the axis.
+     * @param radialSamples Number of triangle samples along the radial.
+     * @param radius        The radius of the cylinder.
+     * @param height        The cylinder's height.
+     */
+    public BoneShape(int axisSamples, int radialSamples,
+                     float radius, float height) {
+        this(axisSamples, radialSamples, radius, height, false);
+    }
+
+    /**
+     * Creates a new Cylinder. By default its center is the origin. Usually, a
+     * higher sample number creates a better looking cylinder, but at the cost
+     * of more vertex information. <br>
+     * If the cylinder is closed the texture is split into axisSamples parts:
+     * top most and bottom most part is used for top and bottom of the cylinder,
+     * rest of the texture for the cylinder wall. The middle of the top is
+     * mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need
+     * a suited distorted texture.
+     *
+     * @param axisSamples   Number of triangle samples along the axis.
+     * @param radialSamples Number of triangle samples along the radial.
+     * @param radius        The radius of the cylinder.
+     * @param height        The cylinder's height.
+     * @param closed        true to create a cylinder with top and bottom surface
+     */
+    public BoneShape(int axisSamples, int radialSamples,
+                     float radius, float height, boolean closed) {
+        this(axisSamples, radialSamples, radius, height, closed, false);
+    }
+
+    /**
+     * Creates a new Cylinder. By default its center is the origin. Usually, a
+     * higher sample number creates a better looking cylinder, but at the cost
+     * of more vertex information. <br>
+     * If the cylinder is closed the texture is split into axisSamples parts:
+     * top most and bottom most part is used for top and bottom of the cylinder,
+     * rest of the texture for the cylinder wall. The middle of the top is
+     * mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need
+     * a suited distorted texture.
+     *
+     * @param axisSamples   Number of triangle samples along the axis.
+     * @param radialSamples Number of triangle samples along the radial.
+     * @param radius        The radius of the cylinder.
+     * @param height        The cylinder's height.
+     * @param closed        true to create a cylinder with top and bottom surface
+     * @param inverted      true to create a cylinder that is meant to be viewed from the
+     *                      interior.
+     */
+    public BoneShape(int axisSamples, int radialSamples,
+                     float radius, float height, boolean closed, boolean inverted) {
+        this(axisSamples, radialSamples, radius, radius, height, closed, inverted);
+    }
+
+    public BoneShape(int axisSamples, int radialSamples,
+                     float radius, float radius2, float height, boolean closed, boolean inverted) {
+        super();
+        updateGeometry(axisSamples, radialSamples, radius, radius2, height, closed, inverted);
+    }
+
+    /**
+     * @return the number of samples along the cylinder axis
+     */
+    public int getAxisSamples() {
+        return axisSamples;
+    }
+
+    /**
+     * @return Returns the height.
+     */
+    public float getHeight() {
+        return height;
+    }
+
+    /**
+     * @return number of samples around cylinder
+     */
+    public int getRadialSamples() {
+        return radialSamples;
+    }
+
+    /**
+     * @return Returns the radius.
+     */
+    public float getRadius() {
+        return radius;
+    }
+
+    public float getRadius2() {
+        return radius2;
+    }
+
+    /**
+     * @return true if end caps are used.
+     */
+    public boolean isClosed() {
+        return closed;
+    }
+
+    /**
+     * @return true if normals and uvs are created for interior use
+     */
+    public boolean isInverted() {
+        return inverted;
+    }
+
+    /**
+     * Rebuilds the cylinder based on a new set of parameters.
+     *
+     * @param axisSamples   the number of samples along the axis.
+     * @param radialSamples the number of samples around the radial.
+     * @param radius        the radius of the bottom of the cylinder.
+     * @param radius2       the radius of the top of the cylinder.
+     * @param height        the cylinder's height.
+     * @param closed        should the cylinder have top and bottom surfaces.
+     * @param inverted      is the cylinder is meant to be viewed from the inside.
+     */
+    public void updateGeometry(int axisSamples, int radialSamples,
+                               float radius, float radius2, float height, boolean closed, boolean inverted) {
+        this.axisSamples = axisSamples + (closed ? 2 : 0);
+        this.radialSamples = radialSamples;
+        this.radius = radius;
+        this.radius2 = radius2;
+        this.height = height;
+        this.closed = closed;
+        this.inverted = inverted;
+
+//        VertexBuffer pvb = getBuffer(Type.Position);
+//        VertexBuffer nvb = getBuffer(Type.Normal);
+//        VertexBuffer tvb = getBuffer(Type.TexCoord);
+
+        // Vertices
+        int vertCount = axisSamples * (radialSamples + 1) + (closed ? 2 : 0);
+
+        setBuffer(Type.Position, 3, createVector3Buffer(getFloatBuffer(Type.Position), vertCount));
+
+        // Normals
+        setBuffer(Type.Normal, 3, createVector3Buffer(getFloatBuffer(Type.Normal), vertCount));
+
+        // Texture co-ordinates
+        setBuffer(Type.TexCoord, 2, createVector2Buffer(vertCount));
+
+        int triCount = ((closed ? 2 : 0) + 2 * (axisSamples - 1)) * radialSamples;
+
+        setBuffer(Type.Index, 3, createShortBuffer(getShortBuffer(Type.Index), 3 * triCount));
+
+        //Color
+        setBuffer(Type.Color, 4, createFloatBuffer(vertCount * 4));
+
+        // generate geometry
+        float inverseRadial = 1.0f / radialSamples;
+        float inverseAxisLess = 1.0f / (closed ? axisSamples - 3 : axisSamples - 1);
+        float inverseAxisLessTexture = 1.0f / (axisSamples - 1);
+        float halfHeight = 0.5f * height;
+
+        // Generate points on the unit circle to be used in computing the mesh
+        // points on a cylinder slice.
+        float[] sin = new float[radialSamples + 1];
+        float[] cos = new float[radialSamples + 1];
+
+        for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
+            float angle = FastMath.TWO_PI * inverseRadial * radialCount;
+            cos[radialCount] = FastMath.cos(angle);
+            sin[radialCount] = FastMath.sin(angle);
+        }
+        sin[radialSamples] = sin[0];
+        cos[radialSamples] = cos[0];
+
+        // calculate normals
+        Vector3f[] vNormals = null;
+        Vector3f vNormal = Vector3f.UNIT_Z;
+
+        if ((height != 0.0f) && (radius != radius2)) {
+            vNormals = new Vector3f[radialSamples];
+            Vector3f vHeight = Vector3f.UNIT_Z.mult(height);
+            Vector3f vRadial = new Vector3f();
+
+            for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
+                vRadial.set(cos[radialCount], sin[radialCount], 0.0f);
+                Vector3f vRadius = vRadial.mult(radius);
+                Vector3f vRadius2 = vRadial.mult(radius2);
+                Vector3f vMantle = vHeight.subtract(vRadius2.subtract(vRadius));
+                Vector3f vTangent = vRadial.cross(Vector3f.UNIT_Z);
+                vNormals[radialCount] = vMantle.cross(vTangent).normalize();
+            }
+        }
+
+        FloatBuffer nb = getFloatBuffer(Type.Normal);
+        FloatBuffer pb = getFloatBuffer(Type.Position);
+        FloatBuffer tb = getFloatBuffer(Type.TexCoord);
+        FloatBuffer cb = getFloatBuffer(Type.Color);
+
+        cb.rewind();
+        for (int i = 0; i < vertCount; i++) {
+            cb.put(0.05f).put(0.05f).put(0.05f).put(1f);
+        }
+
+        // generate the cylinder itself
+        Vector3f tempNormal = new Vector3f();
+        for (int axisCount = 0, i = 0; axisCount < axisSamples; axisCount++, i++) {
+            float axisFraction;
+            float axisFractionTexture;
+            int topBottom = 0;
+            if (!closed) {
+                axisFraction = axisCount * inverseAxisLess; // in [0,1]
+                axisFractionTexture = axisFraction;
+            } else {
+                if (axisCount == 0) {
+                    topBottom = -1; // bottom
+                    axisFraction = 0;
+                    axisFractionTexture = inverseAxisLessTexture;
+                } else if (axisCount == axisSamples - 1) {
+                    topBottom = 1; // top
+                    axisFraction = 1;
+                    axisFractionTexture = 1 - inverseAxisLessTexture;
+                } else {
+                    axisFraction = (axisCount - 1) * inverseAxisLess;
+                    axisFractionTexture = axisCount * inverseAxisLessTexture;
+                }
+            }
+
+            // compute center of slice
+            float z = height * axisFraction;
+            Vector3f sliceCenter = new Vector3f(0, 0, z);
+
+            // compute slice vertices with duplication at end point
+            int save = i;
+            for (int radialCount = 0; radialCount < radialSamples; radialCount++, i++) {
+                float radialFraction = radialCount * inverseRadial; // in [0,1)
+                tempNormal.set(cos[radialCount], sin[radialCount], 0.0f);
+
+                if (vNormals != null) {
+                    vNormal = vNormals[radialCount];
+                } else if (radius == radius2) {
+                    vNormal = tempNormal;
+                }
+
+                if (topBottom == 0) {
+                    if (!inverted)
+                        nb.put(vNormal.x).put(vNormal.y).put(vNormal.z);
+                    else
+                        nb.put(-vNormal.x).put(-vNormal.y).put(-vNormal.z);
+                } else {
+                    nb.put(0).put(0).put(topBottom * (inverted ? -1 : 1));
+                }
+
+                tempNormal.multLocal((radius - radius2) * axisFraction + radius2)
+                        .addLocal(sliceCenter);
+                pb.put(tempNormal.x).put(tempNormal.y).put(tempNormal.z);
+
+                tb.put((inverted ? 1 - radialFraction : radialFraction))
+                        .put(axisFractionTexture);
+            }
+
+            BufferUtils.copyInternalVector3(pb, save, i);
+            BufferUtils.copyInternalVector3(nb, save, i);
+
+            tb.put((inverted ? 0.0f : 1.0f))
+                    .put(axisFractionTexture);
+        }
+
+        if (closed) {
+            pb.put(0).put(0).put(-halfHeight); // bottom center
+            nb.put(0).put(0).put(-1 * (inverted ? -1 : 1));
+            tb.put(0.5f).put(0);
+            pb.put(0).put(0).put(halfHeight); // top center
+            nb.put(0).put(0).put(1 * (inverted ? -1 : 1));
+            tb.put(0.5f).put(1);
+        }
+
+        IndexBuffer ib = getIndexBuffer();
+        int index = 0;
+        // Connectivity
+        for (int axisCount = 0, axisStart = 0; axisCount < axisSamples - 1; axisCount++) {
+            int i0 = axisStart;
+            int i1 = i0 + 1;
+            axisStart += radialSamples + 1;
+            int i2 = axisStart;
+            int i3 = i2 + 1;
+            for (int i = 0; i < radialSamples; i++) {
+                if (closed && axisCount == 0) {
+                    if (!inverted) {
+                        ib.put(index++, i0++);
+                        ib.put(index++, vertCount - 2);
+                        ib.put(index++, i1++);
+                    } else {
+                        ib.put(index++, i0++);
+                        ib.put(index++, i1++);
+                        ib.put(index++, vertCount - 2);
+                    }
+                } else if (closed && axisCount == axisSamples - 2) {
+                    ib.put(index++, i2++);
+                    ib.put(index++, inverted ? vertCount - 1 : i3++);
+                    ib.put(index++, inverted ? i3++ : vertCount - 1);
+                } else {
+                    ib.put(index++, i0++);
+                    ib.put(index++, inverted ? i2 : i1);
+                    ib.put(index++, inverted ? i1 : i2);
+                    ib.put(index++, i1++);
+                    ib.put(index++, inverted ? i2++ : i3++);
+                    ib.put(index++, inverted ? i3++ : i2++);
+                }
+            }
+        }
+
+        updateBound();
+    }
+
+    @Override
+    public void read(JmeImporter e) throws IOException {
+        super.read(e);
+        InputCapsule capsule = e.getCapsule(this);
+        axisSamples = capsule.readInt("axisSamples", 0);
+        radialSamples = capsule.readInt("radialSamples", 0);
+        radius = capsule.readFloat("radius", 0);
+        radius2 = capsule.readFloat("radius2", 0);
+        height = capsule.readFloat("height", 0);
+        closed = capsule.readBoolean("closed", false);
+        inverted = capsule.readBoolean("inverted", false);
+    }
+
+    @Override
+    public void write(JmeExporter e) throws IOException {
+        super.write(e);
+        OutputCapsule capsule = e.getCapsule(this);
+        capsule.write(axisSamples, "axisSamples", 0);
+        capsule.write(radialSamples, "radialSamples", 0);
+        capsule.write(radius, "radius", 0);
+        capsule.write(radius2, "radius2", 0);
+        capsule.write(height, "height", 0);
+        capsule.write(closed, "closed", false);
+        capsule.write(inverted, "inverted", false);
+    }
+
+
+}

+ 238 - 0
jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java

@@ -0,0 +1,238 @@
+package com.jme3.scene.debug.custom;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.bounding.*;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.shape.Sphere;
+
+import static com.jme3.util.BufferUtils.createFloatBuffer;
+
+import java.nio.FloatBuffer;
+import java.util.HashMap;
+
+/**
+ * The class that displays either wires between the bones' heads if no length
+ * data is supplied and full bones' shapes otherwise.
+ */
+public class SkeletonBone extends Node {
+
+    /**
+     * The skeleton to be displayed.
+     */
+    private Skeleton skeleton;
+    /**
+     * The map between the bone index and its length.
+     */
+    private Map<Bone, Node> boneNodes = new HashMap<Bone, Node>();
+    private Map<Node, Bone> nodeBones = new HashMap<Node, Bone>();
+    private Node selectedNode = null;
+    private boolean guessBonesOrientation = false;
+
+    /**
+     * Creates a wire with bone lengths data. If the data is supplied then the
+     * wires will show each full bone (from head to tail).
+     *
+     * @param skeleton    the skeleton that will be shown
+     * @param boneLengths a map between the bone's index and the bone's length
+     */
+    public SkeletonBone(Skeleton skeleton, Map<Integer, Float> boneLengths, boolean guessBonesOrientation) {
+        this.skeleton = skeleton;
+        this.skeleton.reset();
+        this.skeleton.updateWorldVectors();
+        this.guessBonesOrientation = guessBonesOrientation;
+
+        BoneShape boneShape = new BoneShape(5, 12, 0.02f, 0.07f, 1f, false, false);
+        Sphere jointShape = new Sphere(10, 10, 0.1f);
+        jointShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(jointShape.getVertexCount() * 4));
+        FloatBuffer cb = jointShape.getFloatBuffer(VertexBuffer.Type.Color);
+
+        cb.rewind();
+        for (int i = 0; i < jointShape.getVertexCount(); i++) {
+            cb.put(0.05f).put(0.05f).put(0.05f).put(1f);
+        }
+
+        for (Bone bone : skeleton.getRoots()) {
+            createSkeletonGeoms(bone, boneShape, jointShape, boneLengths, skeleton, this, guessBonesOrientation);
+        }
+        this.updateModelBound();
+
+
+        Sphere originShape = new Sphere(10, 10, 0.02f);
+        originShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(originShape.getVertexCount() * 4));
+        cb = originShape.getFloatBuffer(VertexBuffer.Type.Color);
+        cb.rewind();
+        for (int i = 0; i < jointShape.getVertexCount(); i++) {
+            cb.put(0.4f).put(0.4f).put(0.05f).put(1f);
+        }
+
+        Geometry origin = new Geometry("origin", originShape);
+        BoundingVolume bv = this.getWorldBound();
+        float scale = 1;
+        if (bv.getType() == BoundingVolume.Type.AABB) {
+            BoundingBox bb = (BoundingBox) bv;
+            scale = (bb.getXExtent() + bb.getYExtent() + bb.getZExtent()) / 3f;
+        } else if (bv.getType() == BoundingVolume.Type.Sphere) {
+            BoundingSphere bs = (BoundingSphere) bv;
+            scale = bs.getRadius();
+        }
+        origin.scale(scale);
+        attachChild(origin);
+
+
+
+    }
+
+    protected final void createSkeletonGeoms(Bone bone, Mesh boneShape, Mesh jointShape, Map<Integer, Float> boneLengths, Skeleton skeleton, Node parent, boolean guessBonesOrientation) {
+
+        if (guessBonesOrientation && bone.getName().equalsIgnoreCase("Site")) {
+            //BVH skeleton have a useless end point bone named Site
+            return;
+        }
+        Node n = new Node(bone.getName() + "Node");
+        Geometry bGeom = new Geometry(bone.getName(), boneShape);
+        Geometry jGeom = new Geometry(bone.getName() + "Joint", jointShape);
+        n.setLocalTranslation(bone.getLocalPosition());
+        n.setLocalRotation(bone.getLocalRotation());
+
+        float boneLength = boneLengths.get(skeleton.getBoneIndex(bone));
+        n.setLocalScale(bone.getLocalScale());
+
+        bGeom.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X).normalizeLocal());
+
+        if (guessBonesOrientation) {
+            //One child only, the bone direction is from the parent joint to the child joint.
+            if (bone.getChildren().size() == 1) {
+                Vector3f v = bone.getChildren().get(0).getLocalPosition();
+                Quaternion q = new Quaternion();
+                q.lookAt(v, Vector3f.UNIT_Z);
+                bGeom.setLocalRotation(q);
+                boneLength = v.length();
+            }
+            //no child, the bone has the same direction as the parent bone.
+            if (bone.getChildren().isEmpty()) {
+                if (parent.getChildren().size() > 0) {
+                    bGeom.setLocalRotation(parent.getChild(0).getLocalRotation());
+                } else {
+                    //no parent, let's use the bind orientation of the bone
+                    bGeom.setLocalRotation(bone.getBindRotation());
+                }
+            }
+        }
+        bGeom.setLocalScale(boneLength);
+        jGeom.setLocalScale(boneLength);
+
+        n.attachChild(bGeom);
+        n.attachChild(jGeom);
+
+        //tip
+        if (bone.getChildren().size() != 1) {
+            Geometry gt = jGeom.clone();
+            gt.scale(0.8f);
+            Vector3f v = new Vector3f(0, boneLength, 0);
+            if (guessBonesOrientation) {
+                if (bone.getChildren().isEmpty()) {
+                    if (parent.getChildren().size() > 0) {
+                        gt.setLocalTranslation(bGeom.getLocalRotation().mult(parent.getChild(0).getLocalRotation()).mult(v, v));
+                    } else {
+                        gt.setLocalTranslation(bGeom.getLocalRotation().mult(bone.getBindRotation()).mult(v, v));
+                    }
+                }
+            } else {
+                gt.setLocalTranslation(v);
+            }
+
+            n.attachChild(gt);
+        }
+
+
+        boneNodes.put(bone, n);
+        nodeBones.put(n, bone);
+        parent.attachChild(n);
+        for (Bone childBone : bone.getChildren()) {
+            createSkeletonGeoms(childBone, boneShape, jointShape, boneLengths, skeleton, n, guessBonesOrientation);
+        }
+    }
+
+    protected Bone select(Geometry g) {
+        Node parentNode = g.getParent();
+
+        if (parent != null) {
+            Bone b = nodeBones.get(parentNode);
+            if (b != null) {
+                selectedNode = parentNode;
+            }
+            return b;
+        }
+        return null;
+    }
+
+    protected Node getSelectedNode() {
+        return selectedNode;
+    }
+
+
+    protected final void updateSkeletonGeoms(Bone bone) {
+        if (guessBonesOrientation && bone.getName().equalsIgnoreCase("Site")) {
+            return;
+        }
+        Node n = boneNodes.get(bone);
+        n.setLocalTranslation(bone.getLocalPosition());
+        n.setLocalRotation(bone.getLocalRotation());
+        n.setLocalScale(bone.getLocalScale());
+
+        for (Bone childBone : bone.getChildren()) {
+            updateSkeletonGeoms(childBone);
+        }
+    }
+
+    /**
+     * The method updates the geometry according to the positions of the bones.
+     */
+    public void updateGeometry() {
+
+        for (Bone bone : skeleton.getRoots()) {
+            updateSkeletonGeoms(bone);
+        }
+    }
+}

+ 156 - 0
jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java

@@ -0,0 +1,156 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.scene.debug.custom;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.animation.SkeletonControl;
+import com.jme3.app.Application;
+import com.jme3.app.state.AbstractAppState;
+import com.jme3.app.state.AppStateManager;
+import com.jme3.collision.CollisionResults;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Nehon
+ */
+public class SkeletonDebugAppState extends AbstractAppState {
+
+    private Node debugNode = new Node("debugNode");
+    private Map<Skeleton, SkeletonDebugger> skeletons = new HashMap<Skeleton, SkeletonDebugger>();
+    private Map<Skeleton, Bone> selectedBones = new HashMap<Skeleton, Bone>();
+    private Application app;
+
+    @Override
+    public void initialize(AppStateManager stateManager, Application app) {
+        ViewPort vp = app.getRenderManager().createMainView("debug", app.getCamera());
+        vp.attachScene(debugNode);
+        vp.setClearDepth(true);
+        this.app = app;
+        for (SkeletonDebugger skeletonDebugger : skeletons.values()) {
+            skeletonDebugger.initialize(app.getAssetManager());
+        }
+        app.getInputManager().addListener(actionListener, "shoot");
+        app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
+        super.initialize(stateManager, app);
+    }
+
+    @Override
+    public void update(float tpf) {
+        debugNode.updateLogicalState(tpf);
+        debugNode.updateGeometricState();
+    }
+
+    public SkeletonDebugger addSkeleton(SkeletonControl skeletonControl, boolean guessBonesOrientation) {
+        Skeleton skeleton = skeletonControl.getSkeleton();
+        Spatial forSpatial = skeletonControl.getSpatial();
+        return addSkeleton(skeleton, forSpatial, guessBonesOrientation);
+    }
+
+    public SkeletonDebugger addSkeleton(Skeleton skeleton, Spatial forSpatial, boolean guessBonesOrientation) {
+
+        SkeletonDebugger sd = new SkeletonDebugger(forSpatial.getName() + "_Skeleton", skeleton, guessBonesOrientation);
+        sd.setLocalTransform(forSpatial.getWorldTransform());
+        if (forSpatial instanceof Node) {
+            List<Geometry> geoms = new ArrayList<>();
+            findGeoms((Node) forSpatial, geoms);
+            if (geoms.size() == 1) {
+                sd.setLocalTransform(geoms.get(0).getWorldTransform());
+            }
+        }
+        skeletons.put(skeleton, sd);
+        debugNode.attachChild(sd);
+        if (isInitialized()) {
+            sd.initialize(app.getAssetManager());
+        }
+        return sd;
+    }
+
+    private void findGeoms(Node node, List<Geometry> geoms) {
+        for (Spatial spatial : node.getChildren()) {
+            if (spatial instanceof Geometry) {
+                geoms.add((Geometry) spatial);
+            } else if (spatial instanceof Node) {
+                findGeoms((Node) spatial, geoms);
+            }
+        }
+    }
+
+    /**
+     * Pick a Target Using the Mouse Pointer. <ol><li>Map "pick target" action
+     * to a MouseButtonTrigger. <li>flyCam.setEnabled(false);
+     * <li>inputManager.setCursorVisible(true); <li>Implement action in
+     * AnalogListener (TODO).</ol>
+     */
+    private ActionListener actionListener = new ActionListener() {
+        public void onAction(String name, boolean isPressed, float tpf) {
+            if (name.equals("shoot") && isPressed) {
+                CollisionResults results = new CollisionResults();
+                Vector2f click2d = app.getInputManager().getCursorPosition();
+                Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone();
+                Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d);
+                Ray ray = new Ray(click3d, dir);
+
+                debugNode.collideWith(ray, results);
+
+                if (results.size() > 0) {
+                    // The closest result is the target that the player picked:
+                    Geometry target = results.getClosestCollision().getGeometry();
+                    for (SkeletonDebugger skeleton : skeletons.values()) {
+                        Bone selectedBone = skeleton.select(target);
+                        if (selectedBone != null) {
+                            selectedBones.put(skeleton.getSkeleton(), selectedBone);
+                            System.err.println("-----------------------");
+                            System.err.println("Selected Bone : " + selectedBone.getName() + " in skeleton " + skeleton.getName());
+                            System.err.println("Root Bone : " + (selectedBone.getParent() == null));
+                            System.err.println("-----------------------");
+                            System.err.println("Bind translation: " + selectedBone.getBindPosition());
+                            System.err.println("Bind rotation: " + selectedBone.getBindRotation());
+                            System.err.println("Bind scale: " + selectedBone.getBindScale());
+                            System.err.println("---");
+                            System.err.println("Local translation: " + selectedBone.getLocalPosition());
+                            System.err.println("Local rotation: " + selectedBone.getLocalRotation());
+                            System.err.println("Local scale: " + selectedBone.getLocalScale());
+                            System.err.println("---");
+                            System.err.println("Model translation: " + selectedBone.getModelSpacePosition());
+                            System.err.println("Model rotation: " + selectedBone.getModelSpaceRotation());
+                            System.err.println("Model scale: " + selectedBone.getModelSpaceScale());
+                            System.err.println("---");
+                            System.err.println("Bind inverse Transform: ");
+                            System.err.println(selectedBone.getBindInverseTransform());
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    public Map<Skeleton, Bone> getSelectedBones() {
+        return selectedBones;
+    }
+
+    public Node getDebugNode() {
+        return debugNode;
+    }
+
+    public void setDebugNode(Node debugNode) {
+        this.debugNode = debugNode;
+    }
+}

+ 218 - 0
jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java

@@ -0,0 +1,218 @@
+package com.jme3.scene.debug.custom;
+
+/*
+ * 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.
+ */
+
+import com.jme3.animation.Bone;
+
+import java.util.Map;
+
+import com.jme3.animation.Skeleton;
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.BatchNode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * The class that creates a mesh to display how bones behave. If it is supplied
+ * with the bones' lengths it will show exactly how the bones look like on the
+ * scene. If not then only connections between each bone heads will be shown.
+ */
+public class SkeletonDebugger extends BatchNode {
+
+    /**
+     * The lines of the bones or the wires between their heads.
+     */
+    private SkeletonBone bones;
+
+    private Skeleton skeleton;
+    /**
+     * The dotted lines between a bone's tail and the had of its children. Not
+     * available if the length data was not provided.
+     */
+    private SkeletonInterBoneWire interBoneWires;
+    private List<Bone> selectedBones = new ArrayList<Bone>();
+
+    public SkeletonDebugger() {
+    }
+
+    /**
+     * Creates a debugger with no length data. The wires will be a connection
+     * between the bones' heads only. The points will show the bones' heads only
+     * and no dotted line of inter bones connection will be visible.
+     *
+     * @param name     the name of the debugger's node
+     * @param skeleton the skeleton that will be shown
+     */
+    public SkeletonDebugger(String name, Skeleton skeleton, boolean guessBonesOrientation) {
+        super(name);
+        this.skeleton = skeleton;
+        skeleton.reset();
+        skeleton.updateWorldVectors();
+        Map<Integer, Float> boneLengths = new HashMap<Integer, Float>();
+
+        for (Bone bone : skeleton.getRoots()) {
+            computeLength(bone, boneLengths, skeleton);
+        }
+
+        bones = new SkeletonBone(skeleton, boneLengths, guessBonesOrientation);
+
+        this.attachChild(bones);
+
+        interBoneWires = new SkeletonInterBoneWire(skeleton, boneLengths, guessBonesOrientation);
+        Geometry g = new Geometry(name + "_interwires", interBoneWires);
+        g.setBatchHint(BatchHint.Never);
+        this.attachChild(g);
+    }
+
+    protected void initialize(AssetManager assetManager) {
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", new ColorRGBA(0.05f, 0.05f, 0.05f, 1.0f));//new ColorRGBA(0.1f, 0.1f, 0.1f, 1.0f)   
+        setMaterial(mat);
+        Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat2.setBoolean("VertexColor", true);
+        bones.setMaterial(mat2);
+        batch();
+
+    }
+
+    @Override
+    public final void setMaterial(Material material) {
+        if (batches.isEmpty()) {
+            for (int i = 0; i < children.size(); i++) {
+                children.get(i).setMaterial(material);
+            }
+        } else {
+            super.setMaterial(material);
+        }
+    }
+
+    public Skeleton getSkeleton() {
+        return skeleton;
+    }
+
+
+    private void computeLength(Bone b, Map<Integer, Float> boneLengths, Skeleton skeleton) {
+        if (b.getChildren().isEmpty()) {
+            if (b.getParent() != null) {
+                boneLengths.put(skeleton.getBoneIndex(b), boneLengths.get(skeleton.getBoneIndex(b.getParent())) * 0.75f);
+            } else {
+                boneLengths.put(skeleton.getBoneIndex(b), 0.1f);
+            }
+        } else {
+            float length = Float.MAX_VALUE;
+            for (Bone bone : b.getChildren()) {
+                float len = b.getModelSpacePosition().subtract(bone.getModelSpacePosition()).length();
+                if (len < length) {
+                    length = len;
+                }
+            }
+            boneLengths.put(skeleton.getBoneIndex(b), length);
+            for (Bone bone : b.getChildren()) {
+                computeLength(bone, boneLengths, skeleton);
+            }
+        }
+    }
+
+    @Override
+    public void updateLogicalState(float tpf) {
+        super.updateLogicalState(tpf);
+        bones.updateGeometry();
+        if (interBoneWires != null) {
+            interBoneWires.updateGeometry();
+        }
+    }
+
+    ColorRGBA selectedColor = ColorRGBA.Orange;
+    ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f);
+
+    protected Bone select(Geometry g) {
+        Node oldNode = bones.getSelectedNode();
+        Bone b = bones.select(g);
+        if (b == null) {
+            return null;
+        }
+        if (oldNode != null) {
+            markSelected(oldNode, false);
+        }
+        markSelected(bones.getSelectedNode(), true);
+        return b;
+    }
+
+    /**
+     * @return the skeleton wires
+     */
+    public SkeletonBone getBoneShapes() {
+        return bones;
+    }
+
+    /**
+     * @return the dotted line between bones (can be null)
+     */
+    public SkeletonInterBoneWire getInterBoneWires() {
+        return interBoneWires;
+    }
+
+    protected void markSelected(Node n, boolean selected) {
+        ColorRGBA c = baseColor;
+        if (selected) {
+            c = selectedColor;
+        }
+        for (Spatial spatial : n.getChildren()) {
+            if (spatial instanceof Geometry) {
+                Geometry geom = (Geometry) spatial;
+
+                Geometry batch = (Geometry) getChild(getName() + "-batch0");
+                VertexBuffer vb = batch.getMesh().getBuffer(VertexBuffer.Type.Color);
+                FloatBuffer color = (FloatBuffer) vb.getData();
+                //  System.err.println(getName() + "." + geom.getName() + " index " + getGeometryStartIndex(geom) * 4 + "/" + color.limit());
+
+                color.position(getGeometryStartIndex(geom) * 4);
+
+                for (int i = 0; i < geom.getVertexCount(); i++) {
+                    color.put(c.r).put(c.g).put(c.b).put(c.a);
+                }
+                color.rewind();
+                vb.updateData(color);
+            }
+        }
+    }
+}

+ 141 - 0
jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java

@@ -0,0 +1,141 @@
+package com.jme3.scene.debug.custom;
+
+/*
+ * 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.
+ */
+
+
+import java.nio.FloatBuffer;
+import java.util.Map;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+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.util.BufferUtils;
+
+/**
+ * A class that displays a dotted line between a bone tail and its childrens' heads.
+ *
+ * @author Marcin Roguski (Kaelthas)
+ */
+public class SkeletonInterBoneWire extends Mesh {
+    private static final int POINT_AMOUNT = 10;
+    /**
+     * The amount of connections between bones.
+     */
+    private int connectionsAmount;
+    /**
+     * The skeleton that will be showed.
+     */
+    private Skeleton skeleton;
+    /**
+     * The map between the bone index and its length.
+     */
+    private Map<Integer, Float> boneLengths;
+
+    private boolean guessBonesOrientation = false;
+
+    /**
+     * Creates buffers for points. Each line has POINT_AMOUNT of points.
+     *
+     * @param skeleton    the skeleton that will be showed
+     * @param boneLengths the lengths of the bones
+     */
+    public SkeletonInterBoneWire(Skeleton skeleton, Map<Integer, Float> boneLengths, boolean guessBonesOrientation) {
+        this.skeleton = skeleton;
+
+        for (Bone bone : skeleton.getRoots()) {
+            this.countConnections(bone);
+        }
+
+        this.setMode(Mode.Points);
+        this.setPointSize(2);
+        this.boneLengths = boneLengths;
+
+        VertexBuffer pb = new VertexBuffer(Type.Position);
+        FloatBuffer fpb = BufferUtils.createFloatBuffer(POINT_AMOUNT * connectionsAmount * 3);
+        pb.setupData(Usage.Stream, 3, Format.Float, fpb);
+        this.setBuffer(pb);
+
+        this.guessBonesOrientation = guessBonesOrientation;
+        this.updateCounts();
+    }
+
+    /**
+     * The method updates the geometry according to the poitions of the bones.
+     */
+    public void updateGeometry() {
+        VertexBuffer vb = this.getBuffer(Type.Position);
+        FloatBuffer posBuf = this.getFloatBuffer(Type.Position);
+        posBuf.clear();
+        for (int i = 0; i < skeleton.getBoneCount(); ++i) {
+            Bone bone = skeleton.getBone(i);
+            Vector3f parentTail = bone.getModelSpacePosition().add(bone.getModelSpaceRotation().mult(Vector3f.UNIT_Y.mult(boneLengths.get(i))));
+
+            if (guessBonesOrientation) {
+                parentTail = bone.getModelSpacePosition();
+            }
+
+            for (Bone child : bone.getChildren()) {
+                Vector3f childHead = child.getModelSpacePosition();
+                Vector3f v = childHead.subtract(parentTail);
+                float pointDelta = v.length() / POINT_AMOUNT;
+                v.normalizeLocal().multLocal(pointDelta);
+                Vector3f pointPosition = parentTail.clone();
+                for (int j = 0; j < POINT_AMOUNT; ++j) {
+                    posBuf.put(pointPosition.getX()).put(pointPosition.getY()).put(pointPosition.getZ());
+                    pointPosition.addLocal(v);
+                }
+            }
+        }
+        posBuf.flip();
+        vb.updateData(posBuf);
+
+        this.updateBound();
+    }
+
+    /**
+     * Th method couns the connections between bones.
+     *
+     * @param bone the bone where counting starts
+     */
+    private void countConnections(Bone bone) {
+        for (Bone child : bone.getChildren()) {
+            ++connectionsAmount;
+            this.countConnections(child);
+        }
+    }
+}

+ 7 - 9
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java

@@ -31,21 +31,18 @@
  */
 package com.jme3.scene.instancing;
 
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.material.MatParam;
 import com.jme3.material.Material;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.GeometryGroupNode;
-import com.jme3.scene.Mesh;
-import com.jme3.scene.Node;
-import com.jme3.scene.Spatial;
-import com.jme3.scene.UserData;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.*;
 import com.jme3.scene.control.Control;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.material.MatParam;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
@@ -217,6 +214,7 @@ public class InstancedNode extends GeometryGroupNode {
             ig.setMesh(lookUp.mesh);
             ig.setUserData(UserData.JME_PHYSICSIGNORE, true);
             ig.setCullHint(CullHint.Never);
+            ig.setShadowMode(RenderQueue.ShadowMode.Inherit);
             instancesMap.put(lookUp.clone(), ig);
             attachChild(ig);
         }

+ 23 - 1
jme3-core/src/main/java/com/jme3/scene/mesh/IndexBuffer.java

@@ -75,7 +75,29 @@ public abstract class IndexBuffer {
             return new IndexShortBuffer(BufferUtils.createShortBuffer(indexCount));
         }
     }
-    
+
+    /**
+     * @see Buffer#rewind()
+     */
+    public void rewind() {
+        getBuffer().rewind();
+    }
+
+    /**
+     * @return
+     * @see Buffer#remaining()
+     */
+    public int remaining() {
+        return getBuffer().remaining();
+    }
+
+    /**
+     * Returns the vertex index for the current position.
+     *
+     * @return
+     */
+    public abstract int get();
+
     /**
      * Returns the vertex index for the given index in the index buffer.
      * 

+ 6 - 1
jme3-core/src/main/java/com/jme3/scene/mesh/IndexByteBuffer.java

@@ -47,7 +47,12 @@ public class IndexByteBuffer extends IndexBuffer {
         buf = buffer;
         buf.rewind();
     }
-    
+
+    @Override
+    public int get() {
+        return buf.get() & 0x000000FF;
+    }
+
     @Override
     public int get(int i) {
         return buf.get(i) & 0x000000FF;

+ 4 - 0
jme3-core/src/main/java/com/jme3/scene/mesh/IndexIntBuffer.java

@@ -48,6 +48,10 @@ public class IndexIntBuffer extends IndexBuffer {
         buf.rewind();
     }
 
+    @Override
+    public int get() {
+        return buf.get();
+    }
     @Override
     public int get(int i) {
         return buf.get(i);

+ 4 - 0
jme3-core/src/main/java/com/jme3/scene/mesh/IndexShortBuffer.java

@@ -48,6 +48,10 @@ public class IndexShortBuffer extends IndexBuffer {
         buf.rewind();
     }
 
+    @Override
+    public int get() {
+        return buf.get() & 0x0000FFFF;
+    }
     @Override
     public int get(int i) {
         return buf.get(i) & 0x0000FFFF;

+ 18 - 0
jme3-core/src/main/java/com/jme3/scene/mesh/VirtualIndexBuffer.java

@@ -55,6 +55,7 @@ public class VirtualIndexBuffer extends IndexBuffer {
     protected int numVerts = 0;
     protected int numIndices = 0;
     protected Mode meshMode;
+    protected int position = 0;
  
     public VirtualIndexBuffer(int numVerts, Mode meshMode){
         this.numVerts = numVerts;
@@ -86,6 +87,23 @@ public class VirtualIndexBuffer extends IndexBuffer {
         }
     }
 
+    @Override
+    public int get() {
+        int i = get(position);
+        position++;
+        return i;
+    }
+
+    @Override
+    public void rewind() {
+        position = 0;
+    }
+
+    @Override
+    public int remaining() {
+        return numIndices - position;
+    }
+
     @Override
     public int get(int i) {
         if (meshMode == Mode.Triangles || meshMode == Mode.Lines || meshMode == Mode.Points){

+ 51 - 18
jme3-core/src/main/java/com/jme3/shader/Glsl100ShaderGenerator.java

@@ -50,7 +50,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
      * the indentation characters 1à tabulation characters
      */
     private final static String INDENTCHAR = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
-    private ShaderNodeVariable inPosTmp;
+    protected ShaderNodeVariable inPosTmp;
 
     /**
      * creates a Glsl100ShaderGenerator
@@ -110,7 +110,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
     protected void generateVaryings(StringBuilder source, ShaderGenerationInfo info, ShaderType type) {
         source.append("\n");
         for (ShaderNodeVariable var : info.getVaryings()) {
-            declareVarying(source, var, type == ShaderType.Vertex ? false : true);
+            declareVarying(source, var, type != ShaderType.Vertex);
         }
     }
 
@@ -141,7 +141,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
     @Override
     protected void generateStartOfMainSection(StringBuilder source, ShaderGenerationInfo info, ShaderType type) {
         source.append("\n");
-        source.append("void main(){\n");
+        source.append("void main() {\n");
         indent();
         appendIndent(source);
         if (type == ShaderType.Vertex) {
@@ -211,7 +211,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
      * this methods does things in this order : 
      * 
      * 1. declaring and mapping input<br>
-     * variables : variable replaced with MatParams or WorldParams are not
+     * variables : variable replaced with MatParams or WorldParams that are Samplers are not
      * declared and are replaced by the param actual name in the code. For others
      * variables, the name space is appended with a "_" before the variable name
      * in the code to avoid names collision between shaderNodes. <br>
@@ -237,27 +237,59 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
         comment(source, shaderNode, "Begin");
         startCondition(shaderNode.getCondition(), source);
 
-        List<String> declaredInputs = new ArrayList<String>();
+        final List<String> declaredInputs = new ArrayList<>();
+
         for (VariableMapping mapping : shaderNode.getInputMapping()) {
 
-            //all variables fed with a matparam or world param are replaced but the matparam itself
-            //it avoids issue with samplers that have to be uniforms, and it optimize a but the shader code.
-            if (isWorldOrMaterialParam(mapping.getRightVariable())) {
-                nodeSource = replace(nodeSource, mapping.getLeftVariable(), mapping.getRightVariable().getPrefix() + mapping.getRightVariable().getName());
+            final ShaderNodeVariable rightVariable = mapping.getRightVariable();
+            final ShaderNodeVariable leftVariable = mapping.getLeftVariable();
+
+            //Variables fed with a sampler matparam or world param are replaced by the matparam itself
+            //It avoids issue with samplers that have to be uniforms.
+            if (isWorldOrMaterialParam(rightVariable) && rightVariable.getType().startsWith("sampler")) {
+                nodeSource = replace(nodeSource, leftVariable, rightVariable.getPrefix() + rightVariable.getName());
             } else {
-                if (mapping.getLeftVariable().getType().startsWith("sampler")) {
+
+                if (leftVariable.getType().startsWith("sampler")) {
                     throw new IllegalArgumentException("a Sampler must be a uniform");
                 }
+
                 map(mapping, source);
-                String newName = shaderNode.getName() + "_" + mapping.getLeftVariable().getName();
-                if (!declaredInputs.contains(newName)) {
-                    nodeSource = replace(nodeSource, mapping.getLeftVariable(), newName);
-                    declaredInputs.add(newName);
-                }
             }
+
+            String newName = shaderNode.getName() + "_" + leftVariable.getName();
+            if (!declaredInputs.contains(newName)) {
+                nodeSource = replace(nodeSource, leftVariable, newName);
+                declaredInputs.add(newName);
+            }
+        }
+
+        final ShaderNodeDefinition definition = shaderNode.getDefinition();
+
+        for (final ShaderNodeVariable var : definition.getInputs()) {
+
+            if (var.getDefaultValue() == null) {
+                continue;
+            }
+
+            final String fullName = shaderNode.getName() + "_" + var.getName();
+
+            if (declaredInputs.contains(fullName)) {
+                continue;
+            }
+
+            final ShaderNodeVariable variable = new ShaderNodeVariable(var.getType(), shaderNode.getName(),
+                    var.getName(), var.getMultiplicity());
+
+            if (!isVarying(info, variable)) {
+                declareVariable(source, variable, var.getDefaultValue(), true, null);
+            }
+
+            nodeSource = replaceVariableName(nodeSource, variable);
+            declaredInputs.add(fullName);
         }
-       
-        for (ShaderNodeVariable var : shaderNode.getDefinition().getOutputs()) {
+
+        for (ShaderNodeVariable var : definition.getOutputs()) {
             ShaderNodeVariable v = new ShaderNodeVariable(var.getType(), shaderNode.getName(), var.getName(), var.getMultiplicity());
             if (!declaredInputs.contains(shaderNode.getName() + "_" + var.getName())) {
                 if (!isVarying(info, v)) {
@@ -421,6 +453,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
         source.append(" = ");
         String namePrefix = getAppendableNameSpace(mapping.getRightVariable());
         source.append(namePrefix);
+        source.append(mapping.getRightVariable().getPrefix());
         source.append(mapping.getRightVariable().getName());
         if (mapping.getRightSwizzling().length() > 0) {
             source.append(".");
@@ -602,7 +635,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
      * makes sure inPosition attribute is of type vec3 or vec4
      * @param var the inPosition attribute
      */
-    private void fixInPositionType(ShaderNodeVariable var) {
+    protected void fixInPositionType(ShaderNodeVariable var) {
         if(!var.getType().equals("vec3") || !var.getType().equals("vec4")){
             var.setType("vec3");
         }

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác